From 8f48111ed6af4c715abd23359a02799916748ed7 Mon Sep 17 00:00:00 2001 From: "Dr. Dystopia" Date: Tue, 25 Mar 2025 17:38:45 +0100 Subject: [PATCH 001/270] Use structured bindings --- Source/Core/DolphinQt/FIFO/FIFOAnalyzer.cpp | 55 ++++++++++----------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/Source/Core/DolphinQt/FIFO/FIFOAnalyzer.cpp b/Source/Core/DolphinQt/FIFO/FIFOAnalyzer.cpp index 2387ecf458ed..54e2c67b127e 100644 --- a/Source/Core/DolphinQt/FIFO/FIFOAnalyzer.cpp +++ b/Source/Core/DolphinQt/FIFO/FIFOAnalyzer.cpp @@ -156,15 +156,15 @@ void FIFOAnalyzer::UpdateTree() recording_item->addChild(frame_item); - const AnalyzedFrameInfo& frame_info = m_fifo_player.GetAnalyzedFrameInfo(frame); - ASSERT(frame_info.parts.size() != 0); + const auto& [parts, part_type_counts] = m_fifo_player.GetAnalyzedFrameInfo(frame); + ASSERT(parts.size() != 0); Common::EnumMap part_counts; u32 part_start = 0; - for (u32 part_nr = 0; part_nr < frame_info.parts.size(); part_nr++) + for (u32 part_nr = 0; part_nr < parts.size(); part_nr++) { - const auto& part = frame_info.parts[part_nr]; + const auto& part = parts[part_nr]; const u32 part_type_nr = part_counts[part.m_type]; part_counts[part.m_type]++; @@ -190,9 +190,9 @@ void FIFOAnalyzer::UpdateTree() } // We shouldn't end on a Command (it should end with an EFB copy) - ASSERT(part_start == frame_info.parts.size()); + ASSERT(part_start == parts.size()); // The counts we computed should match the frame's counts - ASSERT(std::ranges::equal(frame_info.part_type_counts, part_counts)); + ASSERT(std::ranges::equal(part_type_counts, part_counts)); } } @@ -348,18 +348,18 @@ void FIFOAnalyzer::UpdateDetails() const u32 start_part_nr = items[0]->data(0, PART_START_ROLE).toUInt(); const u32 end_part_nr = items[0]->data(0, PART_END_ROLE).toUInt(); - const AnalyzedFrameInfo& frame_info = m_fifo_player.GetAnalyzedFrameInfo(frame_nr); + const auto& [parts, _part_type_counts] = m_fifo_player.GetAnalyzedFrameInfo(frame_nr); const auto& fifo_frame = m_fifo_player.GetFile()->GetFrame(frame_nr); - const u32 object_start = frame_info.parts[start_part_nr].m_start; - const u32 object_end = frame_info.parts[end_part_nr].m_end; + const u32 object_start = parts[start_part_nr].m_start; + const u32 object_end = parts[end_part_nr].m_end; const u32 object_size = object_end - object_start; u32 object_offset = 0; // NOTE: object_info.m_cpmem is the state of cpmem _after_ all of the commands in this object. // However, it doesn't matter that it doesn't match the start, since it will match by the time // primitives are reached. - auto callback = DetailCallback(frame_info.parts[end_part_nr].m_cpmem); + auto callback = DetailCallback(parts[end_part_nr].m_cpmem); while (object_offset < object_size) { @@ -431,11 +431,11 @@ void FIFOAnalyzer::BeginSearch() const u32 start_part_nr = items[0]->data(0, PART_START_ROLE).toUInt(); const u32 end_part_nr = items[0]->data(0, PART_END_ROLE).toUInt(); - const AnalyzedFrameInfo& frame_info = m_fifo_player.GetAnalyzedFrameInfo(frame_nr); + const auto& [parts, _part_type_counts] = m_fifo_player.GetAnalyzedFrameInfo(frame_nr); const FifoFrameInfo& fifo_frame = m_fifo_player.GetFile()->GetFrame(frame_nr); - const u32 object_start = frame_info.parts[start_part_nr].m_start; - const u32 object_end = frame_info.parts[end_part_nr].m_end; + const u32 object_start = parts[start_part_nr].m_start; + const u32 object_end = parts[end_part_nr].m_end; const u32 object_size = object_end - object_start; const u8* const object = &fifo_frame.fifoData[object_start]; @@ -614,7 +614,7 @@ class DescriptionCallback : public OpcodeDecoder::Callback text = QObject::tr("Primitive %1").arg(QString::fromStdString(name)); text += QLatin1Char{'\n'}; - const auto& vtx_desc = m_cpmem.vtx_desc; + const auto& [low, high] = m_cpmem.vtx_desc; const auto& vtx_attr = m_cpmem.vtx_attr[vat]; u32 i = 0; @@ -670,22 +670,21 @@ class DescriptionCallback : public OpcodeDecoder::Callback ASSERT(i == vertex_num * vertex_size); text += QLatin1Char{'\n'}; - if (vtx_desc.low.PosMatIdx) + if (low.PosMatIdx) process_simple_component(1); - for (auto texmtxidx : vtx_desc.low.TexMatIdx) + for (auto texmtxidx : low.TexMatIdx) { if (texmtxidx) process_simple_component(1); } - process_component(vtx_desc.low.Position, vtx_attr.g0.PosFormat, + process_component(low.Position, vtx_attr.g0.PosFormat, vtx_attr.g0.PosElements == CoordComponentCount::XY ? 2 : 3); - const u32 normal_component_count = - vtx_desc.low.Normal == VertexComponentFormat::Direct ? 3 : 1; + const u32 normal_component_count = low.Normal == VertexComponentFormat::Direct ? 3 : 1; const u32 normal_elements = vtx_attr.g0.NormalElements == NormalComponentCount::NTB ? 3 : 1; - process_component(vtx_desc.low.Normal, vtx_attr.g0.NormalFormat, + process_component(low.Normal, vtx_attr.g0.NormalFormat, normal_component_count * normal_elements, vtx_attr.g0.NormalIndex3 ? normal_elements : 1); - for (u32 c = 0; c < vtx_desc.low.Color.Size(); c++) + for (u32 c = 0; c < low.Color.Size(); c++) { static constexpr Common::EnumMap component_sizes = { 2, // RGB565 @@ -695,7 +694,7 @@ class DescriptionCallback : public OpcodeDecoder::Callback 3, // RGBA6666 4, // RGBA8888 }; - switch (vtx_desc.low.Color[c]) + switch (low.Color[c]) { case VertexComponentFormat::Index8: process_simple_component(1); @@ -710,9 +709,9 @@ class DescriptionCallback : public OpcodeDecoder::Callback break; } } - for (u32 t = 0; t < vtx_desc.high.TexCoord.Size(); t++) + for (u32 t = 0; t < high.TexCoord.Size(); t++) { - process_component(vtx_desc.high.TexCoord[t], vtx_attr.GetTexFormat(t), + process_component(high.TexCoord[t], vtx_attr.GetTexFormat(t), vtx_attr.GetTexElements(t) == TexComponentCount::ST ? 2 : 1); } } @@ -763,15 +762,15 @@ void FIFOAnalyzer::UpdateDescription() const u32 end_part_nr = items[0]->data(0, PART_END_ROLE).toUInt(); const u32 entry_nr = m_detail_list->currentRow(); - const AnalyzedFrameInfo& frame_info = m_fifo_player.GetAnalyzedFrameInfo(frame_nr); + const auto& [parts, _part_type_counts] = m_fifo_player.GetAnalyzedFrameInfo(frame_nr); const FifoFrameInfo& fifo_frame = m_fifo_player.GetFile()->GetFrame(frame_nr); - const u32 object_start = frame_info.parts[start_part_nr].m_start; - const u32 object_end = frame_info.parts[end_part_nr].m_end; + const u32 object_start = parts[start_part_nr].m_start; + const u32 object_end = parts[end_part_nr].m_end; const u32 object_size = object_end - object_start; const u32 entry_start = m_object_data_offsets[entry_nr]; - auto callback = DescriptionCallback(frame_info.parts[end_part_nr].m_cpmem); + auto callback = DescriptionCallback(parts[end_part_nr].m_cpmem); OpcodeDecoder::RunCommand(&fifo_frame.fifoData[object_start + entry_start], object_size - entry_start, callback); m_entry_detail_browser->setText(callback.text); From a327466771dbdcec0f7811b75f6545d74c09ed70 Mon Sep 17 00:00:00 2001 From: "Dr. Dystopia" Date: Fri, 11 Apr 2025 15:19:39 +0200 Subject: [PATCH 002/270] Replace lazy implementation - Tools find-includes-cycles --- Tools/find-includes-cycles.py | 41 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Tools/find-includes-cycles.py b/Tools/find-includes-cycles.py index c4139cd7a0db..00f10daed454 100755 --- a/Tools/find-includes-cycles.py +++ b/Tools/find-includes-cycles.py @@ -1,26 +1,20 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 ''' Run this script from Source/Core/ to find all the #include cycles. ''' -import subprocess +from typing import Iterator +from pathlib import Path -def get_local_includes_for(path): - lines = open(path).read().split('\n') - includes = [l.strip() for l in lines if l.strip().startswith('#include')] - return [i.split()[1][1:-1] for i in includes if '"' in i.split()[1]] +def get_local_includes_for(path: Path) -> Iterator[str]: + with path.open() as file: + for line in file: + line = line.strip() + if line.startswith("#include") and '"' in line: + yield line.split()[1].strip(' "') -def find_all_files(): - '''Could probably use os.walk, but meh.''' - f = subprocess.check_output(['find', '.', '-name', '*.h'], - universal_newlines=True).strip().split('\n') - return [p[2:] for p in f] - -def make_include_graph(): - return { f: get_local_includes_for(f) for f in find_all_files() } - -def strongly_connected_components(graph): +def strongly_connected_components(graph: dict[str, list[str]]) -> list[tuple[str, ...]]: """ Tarjan's Algorithm (named for its discoverer, Robert Tarjan) is a graph theory algorithm for finding the strongly connected components of a graph. @@ -34,7 +28,7 @@ def strongly_connected_components(graph): index = {} result = [] - def strongconnect(node): + def strongconnect(node: str) -> None: # set the depth index for this node to the smallest unused index index[node] = index_counter[0] lowlinks[node] = index_counter[0] @@ -74,7 +68,12 @@ def strongconnect(node): return result if __name__ == '__main__': - comp = strongly_connected_components(make_include_graph()) - for c in comp: - if len(c) != 1: - print(c) + paths = Path(".").glob("**/*.h") + graph = { + path.as_posix(): list(get_local_includes_for(path)) + for path in paths + } + components = strongly_connected_components(graph) + for component in components: + if len(component) != 1: + print(component) From 851c555c90536dd24d03b9b41ccdd778940f6d28 Mon Sep 17 00:00:00 2001 From: "Dr. Dystopia" Date: Sun, 20 Apr 2025 08:14:49 +0200 Subject: [PATCH 003/270] DiscIO: Use views::keys --- Source/Core/DiscIO/VolumeWii.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Core/DiscIO/VolumeWii.cpp b/Source/Core/DiscIO/VolumeWii.cpp index 764dabe54359..16be024420a8 100644 --- a/Source/Core/DiscIO/VolumeWii.cpp +++ b/Source/Core/DiscIO/VolumeWii.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -250,9 +251,9 @@ bool VolumeWii::HasWiiEncryption() const std::vector VolumeWii::GetPartitions() const { - std::vector partitions; - for (const auto& pair : m_partitions) - partitions.push_back(pair.first); + std::vector partitions(m_partitions.size()); + const auto partitions_view = std::views::keys(m_partitions); + std::ranges::copy(partitions_view, partitions.begin()); return partitions; } From b2a57f43915a4dfbde4ab7ca38afbed90dc2edf9 Mon Sep 17 00:00:00 2001 From: techflashYT Date: Wed, 12 Nov 2025 21:21:51 -0700 Subject: [PATCH 004/270] Core/Boot: Fix ELF load address semantics Tested with a binary that has VMA != (LMA | 0x80000000), unlike most libogc binaries. This is indeed a valid setup for ELFs, and one that is generally expected to work properly. Test code: https://github.com/Wii-Linux/NPLL/tree/reloc In either case, you still need to set the PC in the debugger to get it to boot properly, though that is a different issue that I will be fixing in a future patch. Without this patch, the code to be runtime-relocated ends up already loaded at the desired final address, and then when the code tries to relocate from what it thinks is the source address (where the code should be, where the LMA is) to the destination address (the VMA, where Dolphin mistakenly already put the code), it ends up overwriting the code that it is about to execute with garbage, and then promptly crashing. After this patch is applied, the behavior now matches the GameCube with Swiss (assuming a [recent patch](https://github.com/emukidid/swiss-gc/commit/bb4a57186c1481e291b7b0ea6d8e722bab816042) is applied), and the Wii with the Homebrew Channel. --- Source/Core/Core/Boot/ElfReader.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/Boot/ElfReader.cpp b/Source/Core/Core/Boot/ElfReader.cpp index f667387ab3cd..1ef3cd6f6da6 100644 --- a/Source/Core/Core/Boot/ElfReader.cpp +++ b/Source/Core/Core/Boot/ElfReader.cpp @@ -143,17 +143,23 @@ bool ElfReader::LoadIntoMemory(Core::System& system, bool only_in_mem1) const { Elf32_Phdr* p = segments + i; - INFO_LOG_FMT(BOOT, "Type: {} Vaddr: {:08x} Filesz: {} Memsz: {}", p->p_type, p->p_vaddr, - p->p_filesz, p->p_memsz); + INFO_LOG_FMT(BOOT, "Type: {} Vaddr: {:08x} Paddr: {:08x} Filesz: {} Memsz: {}", p->p_type, + p->p_vaddr, p->p_paddr, p->p_filesz, p->p_memsz); if (p->p_type == PT_LOAD) { - u32 writeAddr = p->p_vaddr; + // Check LMA (paddr) first - some are nonsense, so fall back to VMA (vaddr) if invalid + u32 writeAddr = p->p_paddr; + if (writeAddr) + writeAddr |= 0x80000000; // map to virtual address + else + writeAddr = p->p_vaddr; // LMA is empty, fall back to VMA + const u8* src = GetSegmentPtr(i); u32 srcSize = p->p_filesz; u32 dstSize = p->p_memsz; - if (only_in_mem1 && p->p_vaddr >= memory.GetRamSizeReal()) + if (only_in_mem1 && writeAddr >= memory.GetRamSizeReal()) continue; memory.CopyToEmu(writeAddr, src, srcSize); From 56438bde447696f8d5c2aca135d0e75ce88c7b2f Mon Sep 17 00:00:00 2001 From: Sintendo <3380580+Sintendo@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:37:45 +0100 Subject: [PATCH 005/270] PPCSymbolDB: Avoid double lookup --- Source/Core/Core/PowerPC/PPCSymbolDB.cpp | 59 ++++++++++-------------- Source/Core/Core/PowerPC/PPCSymbolDB.h | 1 - 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/Source/Core/Core/PowerPC/PPCSymbolDB.cpp b/Source/Core/Core/PowerPC/PPCSymbolDB.cpp index 250d901bc2a4..5eea9b42bd9e 100644 --- a/Source/Core/Core/PowerPC/PPCSymbolDB.cpp +++ b/Source/Core/Core/PowerPC/PPCSymbolDB.cpp @@ -68,40 +68,39 @@ void PPCSymbolDB::AddKnownSymbol(const Core::CPUThreadGuard& guard, u32 startAdd Common::Symbol::Type type, XFuncMap* functions, XFuncPtrMap* checksum_to_function) { - auto iter = functions->find(startAddr); - if (iter != functions->end()) + const auto [iter, inserted] = functions->try_emplace(startAddr, name); + + Common::Symbol& symbol = iter->second; + symbol.object_name = object_name; + symbol.type = type; + + if (!inserted) { // already got it, let's just update name, checksum & size to be sure. - Common::Symbol* tempfunc = &iter->second; - tempfunc->Rename(name); - tempfunc->object_name = object_name; - tempfunc->hash = HashSignatureDB::ComputeCodeChecksum(guard, startAddr, startAddr + size - 4); - tempfunc->type = type; - tempfunc->size = size; + symbol.Rename(name); + symbol.hash = HashSignatureDB::ComputeCodeChecksum(guard, startAddr, startAddr + size - 4); + symbol.size = size; } else { // new symbol. run analyze. - auto& new_symbol = functions->emplace(startAddr, name).first->second; - new_symbol.object_name = object_name; - new_symbol.type = type; - new_symbol.address = startAddr; + symbol.address = startAddr; - if (new_symbol.type == Common::Symbol::Type::Function) + if (symbol.type == Common::Symbol::Type::Function) { - PPCAnalyst::AnalyzeFunction(guard, startAddr, new_symbol, size); + PPCAnalyst::AnalyzeFunction(guard, startAddr, symbol, size); // Do not truncate symbol when a size is expected - if (size != 0 && new_symbol.size != size) + if (size != 0 && symbol.size != size) { WARN_LOG_FMT(SYMBOLS, "Analysed symbol ({}) size mismatch, {} expected but {} computed", - name, size, new_symbol.size); - new_symbol.size = size; + name, size, symbol.size); + symbol.size = size; } - (*checksum_to_function)[new_symbol.hash].insert(&new_symbol); + (*checksum_to_function)[symbol.hash].insert(&symbol); } else { - new_symbol.size = size; + symbol.size = size; } } } @@ -114,24 +113,14 @@ void PPCSymbolDB::AddKnownNote(u32 start_addr, u32 size, const std::string& name void PPCSymbolDB::AddKnownNote(u32 start_addr, u32 size, const std::string& name, XNoteMap* notes) { - auto iter = notes->find(start_addr); + const auto [iter, inserted] = notes->try_emplace(start_addr); - if (iter != notes->end()) - { - // Already got it, just update the name and size. - Common::Note* tempfunc = &iter->second; - tempfunc->name = name; - tempfunc->size = size; - } - else - { - Common::Note tf; - tf.name = name; - tf.address = start_addr; - tf.size = size; + Common::Note& note = iter->second; + note.name = name; + note.size = size; - (*notes)[start_addr] = tf; - } + if (inserted) + note.address = start_addr; } void PPCSymbolDB::DetermineNoteLayers() diff --git a/Source/Core/Core/PowerPC/PPCSymbolDB.h b/Source/Core/Core/PowerPC/PPCSymbolDB.h index 8ebc26a8108b..fb23e266d6f0 100644 --- a/Source/Core/Core/PowerPC/PPCSymbolDB.h +++ b/Source/Core/Core/PowerPC/PPCSymbolDB.h @@ -4,7 +4,6 @@ #pragma once #include -#include #include "Common/CommonTypes.h" #include "Common/SymbolDB.h" From 50ec81f0c47e24c44a73ba961669972d02280373 Mon Sep 17 00:00:00 2001 From: Reonu Date: Wed, 7 Jan 2026 10:52:59 +0000 Subject: [PATCH 006/270] Add Disable Blur code for Pikmin 1 Wii --- Data/Sys/GameSettings/R9IE01.ini | 6 ++++++ Data/Sys/GameSettings/R9IJ01.ini | 6 ++++++ Data/Sys/GameSettings/R9IK01.ini | 6 ++++++ Data/Sys/GameSettings/R9IP01.ini | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 Data/Sys/GameSettings/R9IE01.ini create mode 100644 Data/Sys/GameSettings/R9IJ01.ini create mode 100644 Data/Sys/GameSettings/R9IK01.ini create mode 100644 Data/Sys/GameSettings/R9IP01.ini diff --git a/Data/Sys/GameSettings/R9IE01.ini b/Data/Sys/GameSettings/R9IE01.ini new file mode 100644 index 000000000000..de46ba7e067b --- /dev/null +++ b/Data/Sys/GameSettings/R9IE01.ini @@ -0,0 +1,6 @@ +# R9IE01 - PIKMIN1 for Wii + +[Gecko] +$Disable Blur [Minty Meeo] +0445ad34 00000000 +* Disables the "Motion Blur" effect that is permanently on during gameplay. diff --git a/Data/Sys/GameSettings/R9IJ01.ini b/Data/Sys/GameSettings/R9IJ01.ini new file mode 100644 index 000000000000..83bfc32649af --- /dev/null +++ b/Data/Sys/GameSettings/R9IJ01.ini @@ -0,0 +1,6 @@ +# R9IJ01 - PIKMIN1 for Wii + +[Gecko] +$Disable Blur [Minty Meeo] +04459B34 00000000 +* Disables the "Motion Blur" effect that is permanently on during gameplay. diff --git a/Data/Sys/GameSettings/R9IK01.ini b/Data/Sys/GameSettings/R9IK01.ini new file mode 100644 index 000000000000..ec33f099cca4 --- /dev/null +++ b/Data/Sys/GameSettings/R9IK01.ini @@ -0,0 +1,6 @@ +# R9IK01 - PIKMIN1 for Wii + +[Gecko] +$Disable Blur [Minty Meeo] +04684354 00000000 +* Disables the "Motion Blur" effect that is permanently on during gameplay. diff --git a/Data/Sys/GameSettings/R9IP01.ini b/Data/Sys/GameSettings/R9IP01.ini new file mode 100644 index 000000000000..197000fcd601 --- /dev/null +++ b/Data/Sys/GameSettings/R9IP01.ini @@ -0,0 +1,6 @@ +# R9IP01 - PIKMIN1 for Wii + +[Gecko] +$Disable Blur [Minty Meeo] +04459D34 00000000 +* Disables the "Motion Blur" effect that is permanently on during gameplay. From 271a140715081f05989b11ab5e9bc754f87935ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Thu, 8 Jan 2026 16:40:43 +0100 Subject: [PATCH 007/270] CCache: Set as opt-in by default --- CMakeLists.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e63c62c66b7..3bd7b7592755 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,6 +110,7 @@ option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence, show the current gam option(USE_MGBA "Enables GBA controllers emulation using libmgba" ON) option(ENABLE_AUTOUPDATE "Enables support for automatic updates" ON) option(USE_RETRO_ACHIEVEMENTS "Enables integration with retroachievements.org" ON) +option(ENABLE_CCACHE "Enables CCache compiler cache" OFF) # Maintainers: if you consider blanket disabling this for your users, please # consider the following points: @@ -207,8 +208,12 @@ if (WIN32) endif() endif() -# setup CCache -include(CCache) +if(ENABLE_CCACHE) + include(CCache) + if(NOT CCACHE_BIN) + message(WARNING "Ccache was enabled but not found. This build will not use Ccache.") + endif() +endif() # Architecture detection and arch specific settings message(STATUS "Detected architecture: ${CMAKE_SYSTEM_PROCESSOR}") From 711c3e1013772634dd8787c659de84aadb9de4c2 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 5 Dec 2025 23:21:17 +0100 Subject: [PATCH 008/270] Jit64: Improve cmpXX imm handling In the past, the register cache would contain either a host register or an immediate (or neither) for a given guest register. Jit64::cmpXX would roll with whichever one it got. But now that PR 12134 is merged, the register cache can contain a host register at the same time as an immediate. This gives us an optimization opportunity in Jit64::cmpXX. Some paths are more efficient when we use an immediate and some paths are more efficient when we don't, so to generate the best code, we should carefully choose if we want an immediate or not rather than leaving it to the register cache. --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 92 ++++++++++++------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 21db558bdb03..64ecd29d75a4 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -568,39 +568,38 @@ void Jit64::cmpXX(UGeckoInstruction inst) u32 crf = inst.CRFD; bool merge_branch = CheckMergedBranch(crf); - bool signedCompare; - RCOpArg comparand; + bool signedCompare = false; + std::optional imm_comparand; switch (inst.OPCD) { // cmp / cmpl case 31: signedCompare = (inst.SUBOP10 == 0); - comparand = signedCompare ? gpr.Use(b, RCMode::Read) : gpr.Bind(b, RCMode::Read); - RegCache::Realize(comparand); + if (gpr.IsImm(b)) + imm_comparand = gpr.Imm32(b); break; // cmpli case 10: signedCompare = false; - comparand = RCOpArg::Imm32((u32)inst.UIMM); + imm_comparand = u32(inst.UIMM); break; // cmpi case 11: signedCompare = true; - comparand = RCOpArg::Imm32((u32)(s32)(s16)inst.UIMM); + imm_comparand = u32(s32(s16(inst.UIMM))); break; default: - signedCompare = false; // silence compiler warning PanicAlertFmt("cmpXX"); } - if (gpr.IsImm(a) && comparand.IsImm()) + if (gpr.IsImm(a) && imm_comparand.has_value()) { // Both registers contain immediate values, so we can pre-compile the compare result - s64 compareResult = signedCompare ? (s64)gpr.SImm32(a) - (s64)comparand.SImm32() : - (u64)gpr.Imm32(a) - (u64)comparand.Imm32(); + s64 compareResult = signedCompare ? s64(gpr.SImm32(a)) - s64(s32(imm_comparand.value())) : + u64(gpr.Imm32(a)) - u64(imm_comparand.value()); if (compareResult == (s32)compareResult) { MOV(64, PPCSTATE_CR(crf), Imm32((u32)compareResult)); @@ -612,15 +611,12 @@ void Jit64::cmpXX(UGeckoInstruction inst) } if (merge_branch) - { - RegCache::Unlock(comparand); DoMergedBranchImmediate(compareResult); - } return; } - if (!gpr.IsImm(a) && !signedCompare && comparand.IsImm() && comparand.Imm32() == 0) + if (!gpr.IsImm(a) && !signedCompare && imm_comparand == u32(0)) { RCX64Reg Ra = gpr.Bind(a, RCMode::Read); RegCache::Realize(Ra); @@ -629,12 +625,38 @@ void Jit64::cmpXX(UGeckoInstruction inst) if (merge_branch) { TEST(64, Ra, Ra); - RegCache::Unlock(comparand, Ra); + RegCache::Unlock(Ra); DoMergedBranchCondition(); } return; } + const bool comparand_needs_reg = + !signedCompare && imm_comparand.has_value() && (imm_comparand.value() & 0x80000000U) != 0; + + RCOpArg Rb; + OpArg comparand; + if (inst.OPCD != 31) + { + // The comparand is from the instruction encoding, so we can't get it from the register cache. + comparand = Imm32(imm_comparand.value()); + } + else if (signedCompare && imm_comparand.has_value()) + { + // Asking the register cache for the comparand might give us a register, but we prefer an imm. + comparand = Imm32(imm_comparand.value()); + } + else if (imm_comparand != u32(0)) + { + // Ask the register cache for the comparand. We prefer getting a register, + // but may also accept imm or memory depending on the circumstances. + Rb = signedCompare ? + gpr.Use(b, RCMode::Read) : + (comparand_needs_reg ? gpr.Bind(b, RCMode::Read) : gpr.BindOrImm(b, RCMode::Read)); + RegCache::Realize(Rb); + comparand = Rb; + } + const X64Reg input = RSCRATCH; if (gpr.IsImm(a)) { @@ -653,25 +675,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) MOVZX(64, 32, input, Ra); } - if (comparand.IsImm()) - { - // sign extension will ruin this, so store it in a register - if (!signedCompare && (comparand.Imm32() & 0x80000000U) != 0) - { - MOV(32, R(RSCRATCH2), comparand); - comparand = RCOpArg::R(RSCRATCH2); - } - } - else - { - if (signedCompare) - { - MOVSX(64, 32, RSCRATCH2, comparand); - comparand = RCOpArg::R(RSCRATCH2); - } - } - - if (comparand.IsImm() && comparand.Imm32() == 0) + if (imm_comparand == u32(0)) { MOV(64, PPCSTATE_CR(crf), R(input)); // Place the comparison next to the branch for macro-op fusion @@ -680,13 +684,31 @@ void Jit64::cmpXX(UGeckoInstruction inst) } else { + if (comparand.IsImm()) + { + if (comparand_needs_reg) + { + // sign extension will ruin this, so store it in a register + MOV(32, R(RSCRATCH2), comparand); + comparand = R(RSCRATCH2); + } + } + else + { + if (signedCompare) + { + MOVSX(64, 32, RSCRATCH2, comparand); + comparand = R(RSCRATCH2); + } + } + SUB(64, R(input), comparand); MOV(64, PPCSTATE_CR(crf), R(input)); } if (merge_branch) { - RegCache::Unlock(comparand); + RegCache::Unlock(Rb); DoMergedBranchCondition(); } } From d75794b774d64f50d4bad334660d8f9068f241bd Mon Sep 17 00:00:00 2001 From: Craig Carnell <1188869+cscd98@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:53:49 +0000 Subject: [PATCH 009/270] Externals: Update cpp-ipc to latest --- Externals/cpp-ipc/cpp-ipc | 2 +- Externals/cpp-ipc/cpp-ipc.vcxproj | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Externals/cpp-ipc/cpp-ipc b/Externals/cpp-ipc/cpp-ipc index ce0773b3e6d5..b6cbbc940e25 160000 --- a/Externals/cpp-ipc/cpp-ipc +++ b/Externals/cpp-ipc/cpp-ipc @@ -1 +1 @@ -Subproject commit ce0773b3e6d5abaa8d104100c5704321113853ca +Subproject commit b6cbbc940e25ff898f09a40b107781cef6027b13 diff --git a/Externals/cpp-ipc/cpp-ipc.vcxproj b/Externals/cpp-ipc/cpp-ipc.vcxproj index 61dc5eda6e8e..749f96f17418 100644 --- a/Externals/cpp-ipc/cpp-ipc.vcxproj +++ b/Externals/cpp-ipc/cpp-ipc.vcxproj @@ -26,6 +26,11 @@ + + + + + @@ -33,10 +38,14 @@ + + + + @@ -55,6 +64,13 @@ + + + + + + + @@ -62,7 +78,6 @@ - From b40047fb6e914a862101da2abeb9e827309cb014 Mon Sep 17 00:00:00 2001 From: Windsurf7 Date: Fri, 13 Feb 2026 03:51:11 +0300 Subject: [PATCH 010/270] GameINI: Add codes to NiGHTS Journey of Dreams This PR provides 60 FPS code additions to NiGHTS: Journey of Dreams. NTSC-U/PAL/NTSC-J: * Added 60 FPS code, coming from the Dolphin wiki. * Followed my code naming convention for Sega games. * The wiki says that the code is for NTSC-U but it's actually region free. It's been confirmed via testing. * The wiki also says that a certain part in the final level softlocks, but that's actually not true and can be bypassed. An explanation has been included with the code. --- Data/Sys/GameSettings/R7E.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Data/Sys/GameSettings/R7E.ini b/Data/Sys/GameSettings/R7E.ini index 24c8fd832a66..fc718a05acf6 100644 --- a/Data/Sys/GameSettings/R7E.ini +++ b/Data/Sys/GameSettings/R7E.ini @@ -12,3 +12,7 @@ [Video_Settings] SafeTextureCacheColorSamples = 0 +[Gecko] +$60FPS [Nick Reynolds] +045524c8 00000001 +*During Will's final stage towards the cutscene with Helen, fly towards the bottom of the level to avoid softlocking. From a0f514cf60d574a95041d7f58779899b0cf28588 Mon Sep 17 00:00:00 2001 From: Windsurf7 Date: Fri, 13 Feb 2026 04:10:37 +0300 Subject: [PATCH 011/270] GameINI: Add codes to Sonic Storybook games This PR provides 60 FPS code additions to Sonic and the Secret Rings & Sonic and the Black Knight. All regions: * Formatting fixes. NTSC-U/PAL: * Added 60 FPS code, coming from the Dolphin wiki. * Followed my code naming convention for Sega games. --- Data/Sys/GameSettings/RENE8P.ini | 5 +++++ Data/Sys/GameSettings/RSR.ini | 2 +- Data/Sys/GameSettings/RSRE8P.ini | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 Data/Sys/GameSettings/RENE8P.ini create mode 100644 Data/Sys/GameSettings/RSRE8P.ini diff --git a/Data/Sys/GameSettings/RENE8P.ini b/Data/Sys/GameSettings/RENE8P.ini new file mode 100644 index 000000000000..ac99386c3754 --- /dev/null +++ b/Data/Sys/GameSettings/RENE8P.ini @@ -0,0 +1,5 @@ +# RENE8P - Sonic and the Black Knight + +[Gecko] +$60FPS [Nick Reynolds] +040887D8 3800003C diff --git a/Data/Sys/GameSettings/RSR.ini b/Data/Sys/GameSettings/RSR.ini index 41d42111b757..90b159671e9e 100644 --- a/Data/Sys/GameSettings/RSR.ini +++ b/Data/Sys/GameSettings/RSR.ini @@ -1,4 +1,4 @@ -# RSRP8P, RSRE8P, RSRJ8P - Sonic and the Secret Rings +# RSRE8P, RSRJ8P, RSRP8P - Sonic and the Secret Rings [Core] # Values set here will override the main Dolphin settings. diff --git a/Data/Sys/GameSettings/RSRE8P.ini b/Data/Sys/GameSettings/RSRE8P.ini new file mode 100644 index 000000000000..40c7b01725fc --- /dev/null +++ b/Data/Sys/GameSettings/RSRE8P.ini @@ -0,0 +1,5 @@ +# RSRE8P - Sonic and the Secret Rings + +[Gecko] +$60FPS [Nick Reynolds] +040B6878 3800003C From b92e98b16c0177c33c6b39e899575225c19bfd36 Mon Sep 17 00:00:00 2001 From: Windsurf7 Date: Fri, 13 Feb 2026 04:13:21 +0300 Subject: [PATCH 012/270] GameINI: Add codes to Sonic Unleashed This PR provides 60 FPS code additions to Sonic Unleashed. NTSC-U: * Added 60 FPS code, coming from the Dolphin wiki. * Followed my code naming convention for Sega games. --- Data/Sys/GameSettings/RSVE8P.ini | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Data/Sys/GameSettings/RSVE8P.ini diff --git a/Data/Sys/GameSettings/RSVE8P.ini b/Data/Sys/GameSettings/RSVE8P.ini new file mode 100644 index 000000000000..a2301386c969 --- /dev/null +++ b/Data/Sys/GameSettings/RSVE8P.ini @@ -0,0 +1,27 @@ +# RSVE8P - Sonic Unleashed + +[Gecko] +$Toggleable 60FPS (Gamecube controller) [yorkiesandskittles, dreamsyntax] +28760BF7 00000008 +04868794 3F800000 +04792460 0000003C +0419D33C 4E800020 +E0000000 80008000 +28760BF7 00000004 +04868794 40000000 +04792460 0000001E +0419D33C 806304C4 +E0000000 80008000 +*Causes many problems during gameplay. Use D-pad Up to toggle to 60 FPS, and D-pad Down to toggle to 30 FPS, as necessary. +$Toggleable 60FPS (Wii controller) [yorkiesandskittles, dreamsyntax] +288144aa 00000008 +04868794 3f800000 +04792460 0000003c +0419d33c 4e800020 +e0000000 80008000 +288144aa 00000004 +04868794 40000000 +04792460 0000001e +0419d33c 806304c4 +e0000000 80008000 +*Causes many problems during gameplay. Use D-pad Up to toggle to 60 FPS, and D-pad Down to toggle to 30 FPS, as necessary. From 8843c14fbad42ef341c26ba9a5f87a787369fa8a Mon Sep 17 00:00:00 2001 From: TryTwo Date: Tue, 24 Feb 2026 01:40:05 -0700 Subject: [PATCH 013/270] Cheat codes: Make newly added codes start disabled. They don't have the call to be activated immediately after being added. --- Source/Core/DolphinQt/Config/ARCodeWidget.cpp | 2 +- Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/DolphinQt/Config/ARCodeWidget.cpp b/Source/Core/DolphinQt/Config/ARCodeWidget.cpp index f2cc8b0e5997..930c27b01b92 100644 --- a/Source/Core/DolphinQt/Config/ARCodeWidget.cpp +++ b/Source/Core/DolphinQt/Config/ARCodeWidget.cpp @@ -279,7 +279,7 @@ void ARCodeWidget::AddCode(ActionReplay::ARCode code) void ARCodeWidget::OnCodeAddClicked() { ActionReplay::ARCode ar; - ar.enabled = true; + ar.enabled = false; m_cheat_code_editor->SetARCode(&ar); if (m_cheat_code_editor->exec() == QDialog::Rejected) diff --git a/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp b/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp index f7917f8f1fac..320b303c48cd 100644 --- a/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp +++ b/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp @@ -218,7 +218,7 @@ void GeckoCodeWidget::OnItemChanged(QListWidgetItem* item) void GeckoCodeWidget::AddCode() { Gecko::GeckoCode code; - code.enabled = true; + code.enabled = false; m_cheat_code_editor->SetGeckoCode(&code); if (m_cheat_code_editor->exec() == QDialog::Rejected) From c8d2568c529b4466ed3de0abcb6cc7c2285078d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Tue, 17 Mar 2026 17:57:19 +0100 Subject: [PATCH 014/270] docs/DSP: Remove DSP_InterC I am not certain what this was used for, but the last non-formatting related change made to it was over 16 years ago. The project targets Windows Vista, uses a Visual Studio 2010 project. Looking at the entry point in `DSP_InterC.cpp`, the program opens a hardcoded path to a DSP rom in `C:\_\dsp_rom.bin` without any documentation on it whatsoever, and decodes something between two mystery addresses before exiting. --- docs/DSP/DSP_InterC/DSP_InterC.sln | 20 - docs/DSP/DSP_InterC/DSP_InterC/DSPExt.cpp | 320 --- docs/DSP/DSP_InterC/DSP_InterC/DSPExt.h | 33 - docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.cpp | 2353 ----------------- docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.h | 48 - docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.cpp | 52 - .../DSP_InterC/DSP_InterC/DSP_InterC.vcxproj | 112 - .../DSP_InterC/DSP_InterC.vcxproj.filters | 54 - docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.cpp | 155 -- docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.h | 23 - .../DSP_InterC/gdsp_opcodes_helper.h | 234 -- docs/DSP/DSP_InterC/DSP_InterC/stdafx.cpp | 23 - docs/DSP/DSP_InterC/DSP_InterC/stdafx.h | 39 - docs/DSP/DSP_InterC/DSP_InterC/targetver.h | 13 - 14 files changed, 3479 deletions(-) delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC.sln delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSPExt.cpp delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSPExt.h delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.cpp delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.h delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.cpp delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj.filters delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.cpp delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.h delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/gdsp_opcodes_helper.h delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/stdafx.cpp delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/stdafx.h delete mode 100644 docs/DSP/DSP_InterC/DSP_InterC/targetver.h diff --git a/docs/DSP/DSP_InterC/DSP_InterC.sln b/docs/DSP/DSP_InterC/DSP_InterC.sln deleted file mode 100644 index 7f42916e05d4..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DSP_InterC", "DSP_InterC\DSP_InterC.vcxproj", "{A010425E-9D5E-461E-910D-0804C2A944D5}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Win32 = Debug|Win32 - Release|Win32 = Release|Win32 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A010425E-9D5E-461E-910D-0804C2A944D5}.Debug|Win32.ActiveCfg = Debug|Win32 - {A010425E-9D5E-461E-910D-0804C2A944D5}.Debug|Win32.Build.0 = Debug|Win32 - {A010425E-9D5E-461E-910D-0804C2A944D5}.Release|Win32.ActiveCfg = Release|Win32 - {A010425E-9D5E-461E-910D-0804C2A944D5}.Release|Win32.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSPExt.cpp b/docs/DSP/DSP_InterC/DSP_InterC/DSPExt.cpp deleted file mode 100644 index f6a4394a23d0..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSPExt.cpp +++ /dev/null @@ -1,320 +0,0 @@ -/*==================================================================== - - filename: opcodes.h - project: GameCube DSP Tool (gcdsp) - created: 2005.03.04 - mail: duddie@walla.com - - Copyright (c) 2005 Duddie - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - ====================================================================*/ -// -// -// At the moment just ls and sl are using the prolog -// perhaps all actions on r03 must be in the prolog -// -#include - -#include "OutBuffer.h" - - - -// -void dsp_op_ext_r_epi(uint16 _Opcode) -{ - uint8 op = (_Opcode >> 2) & 0x3; - uint8 reg = _Opcode & 0x3; - - switch (op) - { - case 0x00: - OutBuffer::AddCode("Error: dsp_op_ext_r_epi"); - break; - - case 0x01: - OutBuffer::AddCode("%s--", OutBuffer::GetRegName(reg)); - // g_dsp.r[reg]--; - break; - - case 0x02: - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(reg)); - //g_dsp.r[reg]++; - break; - - case 0x03: - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(reg), OutBuffer::GetRegName(reg+4)); - // g_dsp.r[reg] += g_dsp.r[reg + 4]; - break; - } -} - - -void dsp_op_ext_mv(uint16 _Opcode) -{ - uint8 sreg = _Opcode & 0x3; - uint8 dreg = ((_Opcode >> 2) & 0x3); - - OutBuffer::AddCode("%s = %s", OutBuffer::GetRegName(dreg + 0x18), OutBuffer::GetRegName(sreg + 0x1c)); - - // g_dsp.r[dreg + 0x18] = g_dsp.r[sreg + 0x1c]; -} - - -void dsp_op_ext_s(uint16 _Opcode) -{ - uint8 dreg = _Opcode & 0x3; - uint8 sreg = ((_Opcode >> 3) & 0x3) + 0x1c; - - OutBuffer::AddCode("WriteDMEM(%s, %s)", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(sreg)); - // dsp_dmem_write(g_dsp.r[dreg], g_dsp.r[sreg]); - - if (_Opcode & 0x04) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(dreg+4)); - // g_dsp.r[dreg] += g_dsp.r[dreg + 4]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(dreg)); - //g_dsp.r[dreg]++; - } -} - - -void dsp_op_ext_l(uint16 _Opcode) -{ - uint8 sreg = _Opcode & 0x3; - uint8 dreg = ((_Opcode >> 3) & 0x7) + 0x18; - - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(sreg)); - // uint16 val = dsp_dmem_read(g_dsp.r[sreg]); - // g_dsp.r[dreg] = val; - - if (_Opcode & 0x04) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(sreg), OutBuffer::GetRegName(sreg+4)); - // g_dsp.r[sreg] += g_dsp.r[sreg + 4]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(sreg)); - // g_dsp.r[sreg]++; - } -} - - -void dsp_op_ext_ls_pro(uint16 _Opcode) -{ - uint8 areg = (_Opcode & 0x1) + 0x1e; - - OutBuffer::AddCode("WriteDMEM(%s, %s)", OutBuffer::GetRegName(0x03), OutBuffer::GetRegName(areg)); - // dsp_dmem_write(g_dsp.r[0x03], g_dsp.r[areg]); - - if (_Opcode & 0x8) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(0x03), OutBuffer::GetRegName(0x07)); - // g_dsp.r[0x03] += g_dsp.r[0x07]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(0x03)); - // g_dsp.r[0x03]++; - } -} - - -void dsp_op_ext_ls_epi(uint16 _Opcode) -{ - uint8 dreg = ((_Opcode >> 4) & 0x3) + 0x18; - - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(0x00)); - // uint16 val = dsp_dmem_read(g_dsp.r[0x00]); - // dsp_op_write_reg(dreg, val); - - if (_Opcode & 0x4) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(0x00), OutBuffer::GetRegName(0x04)); - // g_dsp.r[0x00] += g_dsp.r[0x04]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(0x00)); - // g_dsp.r[0x00]++; - } -} - - -void dsp_op_ext_sl_pro(uint16 _Opcode) -{ - uint8 areg = (_Opcode & 0x1) + 0x1e; - - OutBuffer::AddCode("WriteDMEM(%s, %s)", OutBuffer::GetRegName(0x00), OutBuffer::GetRegName(areg)); - // dsp_dmem_write(g_dsp.r[0x00], g_dsp.r[areg]); - - if (_Opcode & 0x4) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(0x00), OutBuffer::GetRegName(0x04)); - // g_dsp.r[0x00] += g_dsp.r[0x04]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(0x00)); - // g_dsp.r[0x00]++; - } -} - - -void dsp_op_ext_sl_epi(uint16 _Opcode) -{ - uint8 dreg = ((_Opcode >> 4) & 0x3) + 0x18; - - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(0x03)); - // uint16 val = dsp_dmem_read(g_dsp.r[0x03]); - // dsp_op_write_reg(dreg, val); - - if (_Opcode & 0x8) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(0x03), OutBuffer::GetRegName(0x07)); - // g_dsp.r[0x03] += g_dsp.r[0x07]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(0x03)); - // g_dsp.r[0x03]++; - } -} - - -void dsp_op_ext_ld(uint16 _Opcode) -{ - uint8 dreg1 = (((_Opcode >> 5) & 0x1) << 1) + 0x18; - uint8 dreg2 = (((_Opcode >> 4) & 0x1) << 1) + 0x19; - uint8 sreg = _Opcode & 0x3; - - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(dreg1), OutBuffer::GetRegName(sreg)); - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(dreg2), OutBuffer::GetRegName(0x03)); - - // g_dsp.r[dreg1] = dsp_dmem_read(g_dsp.r[sreg]); - // g_dsp.r[dreg2] = dsp_dmem_read(g_dsp.r[0x03]); - - if (_Opcode & 0x04) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(sreg), OutBuffer::GetRegName(sreg + 0x04)); - // g_dsp.r[sreg] += g_dsp.r[sreg + 0x04]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(sreg)); - // g_dsp.r[sreg]++; - } - - if (_Opcode & 0x08) - { - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(0x03), OutBuffer::GetRegName(sreg + 0x07)); - // g_dsp.r[0x03] += g_dsp.r[0x07]; - } - else - { - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(0x03)); - // g_dsp.r[0x03]++; - } -} - - -// ================================================================================ -// -// -// -// ================================================================================ - -void dsp_op_ext_ops_pro(uint16 _Opcode) -{ - if ((_Opcode & 0xFF) == 0){return;} - - switch ((_Opcode >> 4) & 0xf) - { - case 0x00: - dsp_op_ext_r_epi(_Opcode); - break; - - case 0x01: - dsp_op_ext_mv(_Opcode); - break; - - case 0x02: - case 0x03: - dsp_op_ext_s(_Opcode); - break; - - case 0x04: - case 0x05: - case 0x06: - case 0x07: - dsp_op_ext_l(_Opcode); - break; - - case 0x08: - case 0x09: - case 0x0a: - case 0x0b: - - if (_Opcode & 0x2) - { - dsp_op_ext_sl_pro(_Opcode); - } - else - { - dsp_op_ext_ls_pro(_Opcode); - } - - return; - - case 0x0c: - case 0x0d: - case 0x0e: - case 0x0f: - dsp_op_ext_ld(_Opcode); - break; - } -} - - -void dsp_op_ext_ops_epi(uint16 _Opcode) -{ - if ((_Opcode & 0xFF) == 0){return;} - - switch ((_Opcode >> 4) & 0xf) - { - case 0x08: - case 0x09: - case 0x0a: - case 0x0b: - - if (_Opcode & 0x2) - { - dsp_op_ext_sl_epi(_Opcode); - } - else - { - dsp_op_ext_ls_epi(_Opcode); - } - - return; - } -} - - diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSPExt.h b/docs/DSP/DSP_InterC/DSP_InterC/DSPExt.h deleted file mode 100644 index b27a5bbcca1e..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSPExt.h +++ /dev/null @@ -1,33 +0,0 @@ -/*==================================================================== - - filename: opcodes.h - project: GameCube DSP Tool (gcdsp) - created: 2005.03.04 - mail: duddie@walla.com - - Copyright (c) 2005 Duddie - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - ====================================================================*/ - -#ifndef _GDSP_EXT_OP_H -#define _GDSP_EXT_OP_H - -void dsp_op_ext_ops_pro(uint16 _Opcode); -void dsp_op_ext_ops_epi(uint16 _Opcode); - - -#endif diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.cpp b/docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.cpp deleted file mode 100644 index 1352f19a52c9..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.cpp +++ /dev/null @@ -1,2353 +0,0 @@ -#include - -#include "DSPOpcodes.h" -#include "DSPExt.h" - -#define SR_CMP_MASK 0x3f -#define DSP_REG_MASK 0x1f - -/* - -bool CheckCondition(uint8 _Condition) -{ - bool taken = false; - - switch (_Condition & 0xf) - { - case 0x0: - - if ((!(g_dsp.r[R_SR] & 0x02)) && (!(g_dsp.r[R_SR] & 0x08))) - { - taken = true; - } - - break; - - case 0x3: - - if ((g_dsp.r[R_SR] & 0x02) || (g_dsp.r[R_SR] & 0x04) || (g_dsp.r[R_SR] & 0x08)) - { - taken = true; - } - - break; - - // old from duddie - case 0x1: // seems okay - - if ((!(g_dsp.r[R_SR] & 0x02)) && (g_dsp.r[R_SR] & 0x08)) - { - taken = true; - } - - break; - - case 0x2: - - if (!(g_dsp.r[R_SR] & 0x08)) - { - taken = true; - } - - break; - - case 0x4: - - if (!(g_dsp.r[R_SR] & 0x04)) - { - taken = true; - } - - break; - - case 0x5: - - if (g_dsp.r[R_SR] & 0x04) - { - taken = true; - } - - break; - - case 0xc: - - if (!(g_dsp.r[R_SR] & 0x40)) - { - taken = true; - } - - break; - - case 0xd: - - if (g_dsp.r[R_SR] & 0x40) - { - taken = true; - } - - break; - - case 0xf: - taken = true; - break; - - default: - // DebugLog("Unknown condition check: 0x%04x\n", _Condition & 0xf); - break; - } - - return(taken); -} -*/ - -// ======================================================================= - -//------------------------------------------------------------------------------- -void (*dsp_op[])(uint16 opc) = -{ - dsp_op0, dsp_op1, dsp_op2, dsp_op3, - dsp_op4, dsp_op5, dsp_op6, dsp_op7, - dsp_op8, dsp_op9, dsp_opab, dsp_opab, - dsp_opcd, dsp_opcd, dsp_ope, dsp_opf, -}; - -void DecodeOpcode(uint16 op) -{ - dsp_op[op >> 12](op); - -} -void dsp_op_unknown(uint16 opc) -{ - ErrorLog("dsp_op_unknown somewhere"); - OutBuffer::AddCode("unknown somewhere"); -} - - -void dsp_opc_call(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_call"); - -/* uint16 dest = FetchOpcode(); - if (CheckCondition(opc & 0xf)) - { - dsp_reg_store_stack(DSP_STACK_C, g_dsp.pc); - g_dsp.pc = dest; - } */ -} - - -void dsp_opc_ifcc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_call"); - -/* if (!CheckCondition(opc & 0xf)) - { - FetchOpcode(); // skip the next opcode - }*/ -} - - -void dsp_opc_jcc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_call"); - -/* uint16 dest = FetchOpcode(); - - if (CheckCondition(opc & 0xf)) - { - g_dsp.pc = dest; - }*/ -} - - -void dsp_opc_jmpa(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_call"); - -/* uint8 reg; - uint16 addr; - - if ((opc & 0xf) != 0xf) - { - ErrorLog("dsp_opc_jmpa"); - } - - reg = (opc >> 5) & 0x7; - addr = dsp_op_read_reg(reg); - - if (opc & 0x0010) - { - // CALLA - dsp_reg_store_stack(DSP_STACK_C, g_dsp.pc); - } - - g_dsp.pc = addr;*/ -} - - -// NEW (added condition check) -void dsp_opc_ret(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_ret"); - -/* if (CheckCondition(opc & 0xf)) - { - g_dsp.pc = dsp_reg_load_stack(DSP_STACK_C); - }*/ -} - - -void dsp_opc_rti(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_rti"); - -/* if ((opc & 0xf) != 0xf) - { - ErrorLog("dsp_opc_rti"); - } - - g_dsp.r[R_SR] = dsp_reg_load_stack(DSP_STACK_D); - g_dsp.pc = dsp_reg_load_stack(DSP_STACK_C); - - g_dsp.exception_in_progress_hack = false;*/ -} - - -void dsp_opc_halt(uint16 opc) -{ - OutBuffer::AddCode("HALT"); - -/* g_dsp.cr |= 0x4; - g_dsp.pc = g_dsp.err_pc;*/ -} - - -void dsp_opc_loop(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_loop"); - -/* uint16 reg = opc & 0x1f; - uint16 cnt = g_dsp.r[reg]; - uint16 loop_pc = g_dsp.pc; - - while (cnt--) - { - gdsp_loop_step(); - g_dsp.pc = loop_pc; - } - - g_dsp.pc = loop_pc + 1; */ -} - - -void dsp_opc_loopi(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_loopi"); - -/* uint16 cnt = opc & 0xff; - uint16 loop_pc = g_dsp.pc; - - while (cnt--) - { - gdsp_loop_step(); - g_dsp.pc = loop_pc; - } - - g_dsp.pc = loop_pc + 1;*/ -} - - -void dsp_opc_bloop(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_bloop"); - -/* uint16 reg = opc & 0x1f; - uint16 cnt = g_dsp.r[reg]; - uint16 loop_pc = FetchOpcode(); - - if (cnt) - { - dsp_reg_store_stack(0, g_dsp.pc); - dsp_reg_store_stack(2, loop_pc); - dsp_reg_store_stack(3, cnt); - } - else - { - g_dsp.pc = loop_pc + 1; - }*/ -} - - -void dsp_opc_bloopi(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_bloopi"); - -/* uint16 cnt = opc & 0xff; - uint16 loop_pc = FetchOpcode(); - - if (cnt) - { - dsp_reg_store_stack(0, g_dsp.pc); - dsp_reg_store_stack(2, loop_pc); - dsp_reg_store_stack(3, cnt); - } - else - { - g_dsp.pc = loop_pc + 1; - }*/ -} - - -//------------------------------------------------------------- - - -void dsp_opc_mrr(uint16 opc) -{ - uint8 sreg = opc & 0x1f; - uint8 dreg = (opc >> 5) & 0x1f; - - OutBuffer::AddCode("%s = %s", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(sreg)); - -// uint16 val = dsp_op_read_reg(sreg); -// dsp_op_write_reg(dreg, val); -} - - -void dsp_opc_lrr(uint16 opc) -{ - uint8 sreg = (opc >> 5) & 0x3; - uint8 dreg = opc & 0x1f; - - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(sreg)); - -// uint16 val = dsp_dmem_read(g_dsp.r[sreg]); -// dsp_op_write_reg(dreg, val); - - // post processing of source reg - switch ((opc >> 7) & 0x3) - { - case 0x0: // LRR - break; - - case 0x1: // LRRD - OutBuffer::AddCode("%s--", OutBuffer::GetRegName(sreg)); - // g_dsp.r[sreg]--; - break; - - case 0x2: // LRRI - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(sreg)); - //g_dsp.r[sreg]++; - break; - - case 0x3: - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(sreg), OutBuffer::GetRegName(sreg+4)); - // g_dsp.r[sreg] += g_dsp.r[sreg + 4]; - break; - } -} - - -void dsp_opc_srr(uint16 opc) -{ - uint8 dreg = (opc >> 5) & 0x3; - uint8 sreg = opc & 0x1f; - - OutBuffer::AddCode("WriteDMEM(%s, %s)", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(sreg)); - - //uint16 val = dsp_op_read_reg(sreg); - // dsp_dmem_write(g_dsp.r[dreg], val); - - // post processing of source reg - switch ((opc >> 7) & 0x3) - { - case 0x0: // SRR - break; - - case 0x1: // SRRD - OutBuffer::AddCode("%s--", OutBuffer::GetRegName(dreg)); - // g_dsp.r[dreg]--; - break; - - case 0x2: // SRRI - OutBuffer::AddCode("%s++", OutBuffer::GetRegName(dreg)); - // g_dsp.r[dreg]++; - break; - - case 0x3: // SRRX - OutBuffer::AddCode("%s += %s", OutBuffer::GetRegName(dreg), OutBuffer::GetRegName(dreg+4)); - //g_dsp.r[dreg] += g_dsp.r[dreg + 4]; - break; - } -} - - -void dsp_opc_ilrr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_ilrr"); - -/* uint16 reg = opc & 0x3; - uint16 dreg = 0x1e + ((opc >> 8) & 1); - - // always to acc0 ? - g_dsp.r[dreg] = dsp_imem_read(g_dsp.r[reg]); - - switch ((opc >> 2) & 0x3) - { - case 0x0: // no change - break; - - case 0x1: // post decrement - g_dsp.r[reg]--; - break; - - case 0x2: // post increment - g_dsp.r[reg]++; - break; - - default: - ErrorLog("dsp_opc_ilrr"); - }*/ -} - - -void dsp_opc_lri(uint16 opc) -{ - uint8 reg = opc & DSP_REG_MASK; - uint16 imm = FetchOpcode(); - - OutBuffer::AddCode("%s = 0x%02x", OutBuffer::GetRegName(reg), imm); - // dsp_op_write_reg(reg, imm); -} - - -void dsp_opc_lris(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_lris"); - -/* uint8 reg = ((opc >> 8) & 0x7) + 0x18; - uint16 imm = (sint8)opc; - dsp_op_write_reg(reg, imm);*/ -} - - -void dsp_opc_lr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_lr"); - -/* uint8 reg = opc & DSP_REG_MASK; - uint16 addr = FetchOpcode(); - uint16 val = dsp_dmem_read(addr); - dsp_op_write_reg(reg, val);*/ -} - - -void dsp_opc_sr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_sr"); - - /*uint8 reg = opc & DSP_REG_MASK; - uint16 addr = FetchOpcode(); - uint16 val = dsp_op_read_reg(reg); - dsp_dmem_write(addr, val);*/ -} - - -void dsp_opc_si(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_si"); - - //uint16 addr = (sint8)opc; - //uint16 imm = readimemory(); - //dsp_dmem_write(addr, imm); -} - - -void dsp_opc_tstaxh(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_tstaxh"); - - //uint8 reg = (opc >> 8) & 0x1; - //sint16 val = dsp_get_ax_h(reg); - - //Update_SR_Register(val); -} - - -void dsp_opc_clr(uint16 opc) -{ - uint8 reg = (opc >> 11) & 0x1; - - OutBuffer::AddCode("ACC%i = 0", reg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", reg); - - //dsp_set_long_acc(reg, 0); - - //Update_SR_Register((sint64)0); -} - - -void dsp_opc_clrp(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_clrp"); - - //g_dsp.r[0x14] = 0x0000; - //g_dsp.r[0x15] = 0xfff0; - //g_dsp.r[0x16] = 0x00ff; - //g_dsp.r[0x17] = 0x0010; -} - - -// NEW -void dsp_opc_mulc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulc"); - - // math new prod - //uint8 sreg = (opc >> 11) & 0x1; - //uint8 treg = (opc >> 12) & 0x1; - - //sint64 prod = dsp_get_acc_m(sreg) * dsp_get_ax_h(treg) * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_mulcmvz(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulcmvz"); - - ErrorLog("dsp_opc_mulcmvz ni"); -} - - -// NEW -void dsp_opc_mulcmv(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulcmv"); - - ErrorLog("dsp_opc_mulcmv ni"); -} - - -void dsp_opc_cmpar(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_cmpar"); - - //uint8 rreg = ((opc >> 12) & 0x1) + 0x1a; - //uint8 areg = (opc >> 11) & 0x1; - - //// we compare - //sint64 rr = (sint16)g_dsp.r[rreg]; - //rr <<= 16; - - //sint64 ar = dsp_get_long_acc(areg); - - //Update_SR_Register(ar - rr); -} - - -void dsp_opc_cmp(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_cmp"); - - //sint64 acc0 = dsp_get_long_acc(0); - //sint64 acc1 = dsp_get_long_acc(1); - - //Update_SR_Register(acc0 - acc1); -} - - -void dsp_opc_tsta(uint16 opc) -{ - uint8 reg = (opc >> 11) & 0x1; - OutBuffer::AddCode("Update_SR_Register(ACC%i)", reg); - - //uint8 reg = (opc >> 11) & 0x1; - //sint64 acc = dsp_get_long_acc(reg); - - //Update_SR_Register(acc); -} - - -// NEW -void dsp_opc_addaxl(uint16 opc) -{ - uint8 sreg = (opc >> 9) & 0x1; - uint8 dreg = (opc >> 8) & 0x1; - - OutBuffer::AddCode("ACC%i += AX%i_l", dreg, sreg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", dreg); - - //sint64 acc = dsp_get_long_acc(dreg); - //sint64 acx = dsp_get_ax_l(sreg); - - //acc += acx; - - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -// NEW -void dsp_opc_addarn(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_addarn"); - - //uint8 dreg = opc & 0x3; - //uint8 sreg = (opc >> 2) & 0x3; - - //g_dsp.r[dreg] += (sint16)g_dsp.r[0x04 + sreg]; -} - - -// NEW -void dsp_opc_mulcac(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulcac"); - - //sint64 TempProd = dsp_get_long_prod(); - - //// update prod - //uint8 sreg = (opc >> 12) & 0x1; - //sint64 Prod = (sint64)dsp_get_acc_m(sreg) * (sint64)dsp_get_acc_h(sreg) * GetMultiplyModifier(); - //dsp_set_long_prod(Prod); - - //// update acc - //uint8 rreg = (opc >> 8) & 0x1; - //dsp_set_long_acc(rreg, TempProd); -} - - -// NEW -void dsp_opc_movr(uint16 opc) -{ - uint8 areg = (opc >> 8) & 0x1; - uint8 sreg = ((opc >> 9) & 0x3) + 0x18; - - OutBuffer::AddCode("ACC%i = (%s << 16) & ~0xFFFF", areg, OutBuffer::GetRegName(sreg)); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", areg); - - //sint64 acc = (sint16)g_dsp.r[sreg]; - //acc <<= 16; - //acc &= ~0xffff; - - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_movax(uint16 opc) -{ - uint8 sreg = (opc >> 9) & 0x1; - uint8 dreg = (opc >> 8) & 0x1; - - OutBuffer::AddCode("%s = %s", OutBuffer::GetRegName(0x1c + dreg), OutBuffer::GetRegName(0x18 + sreg)); - OutBuffer::AddCode("%s = %s", OutBuffer::GetRegName(0x1e + dreg), OutBuffer::GetRegName(0x1a + sreg)); - - //g_dsp.r[0x1c + dreg] = g_dsp.r[0x18 + sreg]; - //g_dsp.r[0x1e + dreg] = g_dsp.r[0x1a + sreg]; - - OutBuffer::AddCode("%s = (%s < 0) ? 0xFFFF : 0x0000", OutBuffer::GetRegName(0x10 + dreg), - OutBuffer::GetRegName(0x1a + sreg)); - - //if ((sint16)g_dsp.r[0x1a + sreg] < 0) - //{ - // g_dsp.r[0x10 + dreg] = 0xffff; - //} - //else - //{ - // g_dsp.r[0x10 + dreg] = 0; - //} - - dsp_opc_tsta(dreg << 11); -} - - -// NEW -void dsp_opc_xorr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_xorr"); - - //uint8 sreg = (opc >> 9) & 0x1; - //uint8 dreg = (opc >> 8) & 0x1; - - //g_dsp.r[0x1e + dreg] ^= g_dsp.r[0x1a + sreg]; - - //dsp_opc_tsta(dreg << 11); -} - - -void dsp_opc_andr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_andr"); - - //uint8 sreg = (opc >> 9) & 0x1; - //uint8 dreg = (opc >> 8) & 0x1; - - //g_dsp.r[0x1e + dreg] &= g_dsp.r[0x1a + sreg]; - - //dsp_opc_tsta(dreg << 11); -} - - -// NEW -void dsp_opc_orr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_orr"); - - //uint8 sreg = (opc >> 9) & 0x1; - //uint8 dreg = (opc >> 8) & 0x1; - - //g_dsp.r[0x1e + dreg] |= g_dsp.r[0x1a + sreg]; - - //dsp_opc_tsta(dreg << 11); -} - - -// NEW -void dsp_opc_andc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_andc"); - - //uint8 D = (opc >> 8) & 0x1; - - //uint16 ac1 = dsp_get_acc_m(D); - //uint16 ac2 = dsp_get_acc_m(1 - D); - - //dsp_set_long_acc(D, ac1 & ac2); - - //if ((ac1 & ac2) == 0) - //{ - // g_dsp.r[R_SR] |= 0x20; - //} - //else - //{ - // g_dsp.r[R_SR] &= ~0x20; - //} -} - - -//------------------------------------------------------------- - -void dsp_opc_nx(uint16 opc) -{} - - -// NEW -void dsp_opc_andfc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_andfc"); - - //if (opc & 0xf) - //{ - // ErrorLog("dsp_opc_andfc"); - //} - - //uint8 reg = (opc >> 8) & 0x1; - //uint16 imm = FetchOpcode(); - //uint16 val = dsp_get_acc_m(reg); - - //if ((val & imm) == imm) - //{ - // g_dsp.r[R_SR] |= 0x40; - //} - //else - //{ - // g_dsp.r[R_SR] &= ~0x40; - //} -} - - -void dsp_opc_andf(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_andf"); - - //uint8 reg; - //uint16 imm; - //uint16 val; - - //if (opc & 0xf) - //{ - // ErrorLog("dsp_opc_andf"); - //} - - //reg = 0x1e + ((opc >> 8) & 0x1); - //imm = FetchOpcode(); - //val = g_dsp.r[reg]; - - //if ((val & imm) == 0) - //{ - // g_dsp.r[R_SR] |= 0x40; - //} - //else - //{ - // g_dsp.r[R_SR] &= ~0x40; - //} -} - - -void dsp_opc_subf(uint16 opc) -{ - if (opc & 0xf) - { - ErrorLog("dsp_opc_subf"); - } - - uint8 reg = 0x1e + ((opc >> 8) & 0x1); - sint64 imm = (sint16)FetchOpcode(); - - OutBuffer::AddCode("missing: dsp_opc_subf"); - - //sint64 val = (sint16)g_dsp.r[reg]; - //sint64 res = val - imm; - - //Update_SR_Register(res); -} - - -void dsp_opc_xori(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_xori"); - - //if (opc & 0xf) - //{ - // ErrorLog("dsp_opc_xori"); - //} - - //uint8 reg = 0x1e + ((opc >> 8) & 0x1); - //uint16 imm = FetchOpcode(); - //g_dsp.r[reg] ^= imm; - - //Update_SR_Register((sint16)g_dsp.r[reg]); -} - - -void dsp_opc_andi(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_andi"); - - //if (opc & 0xf) - //{ - // ErrorLog("dsp_opc_andi"); - //} - - //uint8 reg = 0x1e + ((opc >> 8) & 0x1); - //uint16 imm = FetchOpcode(); - //g_dsp.r[reg] &= imm; - - //Update_SR_Register((sint16)g_dsp.r[reg]); -} - - -// F|RES: i am not sure if this shouldnt be the whole ACC -// -void dsp_opc_ori(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_ori"); - - //if (opc & 0xf) - //{ - // return(ErrorLog("dsp_opc_ori")); - //} - - //uint8 reg = 0x1e + ((opc >> 8) & 0x1); - //uint16 imm = FetchOpcode(); - //g_dsp.r[reg] |= imm; - - //Update_SR_Register((sint16)g_dsp.r[reg]); -} - - -//------------------------------------------------------------- - -void dsp_opc_add(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_add"); - - //uint8 areg = (opc >> 8) & 0x1; - //sint64 acc0 = dsp_get_long_acc(0); - //sint64 acc1 = dsp_get_long_acc(1); - - //sint64 res = acc0 + acc1; - - //dsp_set_long_acc(areg, res); - - //Update_SR_Register(res); -} - - -//------------------------------------------------------------- - -void dsp_opc_addp(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_addp"); - - //uint8 dreg = (opc >> 8) & 0x1; - //sint64 acc = dsp_get_long_acc(dreg); - //acc = acc + dsp_get_long_prod(); - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_cmpis(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_cmpis"); - - //uint8 areg = (opc >> 8) & 0x1; - - //sint64 acc = dsp_get_long_acc(areg); - //sint64 val = (sint8)opc; - //val <<= 16; - - //sint64 res = acc - val; - - //Update_SR_Register(res); -} - - -// NEW -// verified at the console -void dsp_opc_addpaxz(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_addpaxz"); - - //uint8 dreg = (opc >> 8) & 0x1; - //uint8 sreg = (opc >> 9) & 0x1; - - //sint64 prod = dsp_get_long_prod() & ~0x0ffff; - //sint64 ax_h = dsp_get_long_acx(sreg); - //sint64 acc = (prod + ax_h) & ~0x0ffff; - - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -// NEW -void dsp_opc_movpz(uint16 opc) -{ - uint8 dreg = (opc >> 8) & 0x01; - - OutBuffer::AddCode("ACC%i = PROD & ~0xffff", dreg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", dreg); - - //// overwrite acc and clear low part - //sint64 prod = dsp_get_long_prod(); - //sint64 acc = prod & ~0xffff; - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_decm(uint16 opc) -{ - uint8 dreg = (opc >> 8) & 0x01; - - OutBuffer::AddCode("ACC%i -= 0x10000", dreg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", dreg); - - //sint64 sub = 0x10000; - //sint64 acc = dsp_get_long_acc(dreg); - //acc -= sub; - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_dec(uint16 opc) -{ - uint8 dreg = (opc >> 8) & 0x01; - - OutBuffer::AddCode("ACC%i -= 1", dreg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", dreg); - - //sint64 acc = dsp_get_long_acc(dreg) - 1; - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_incm(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_incm"); - - //uint8 dreg = (opc >> 8) & 0x1; - - //sint64 sub = 0x10000; - //sint64 acc = dsp_get_long_acc(dreg); - //acc += sub; - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_inc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_inc"); - - //uint8 dreg = (opc >> 8) & 0x1; - - //sint64 acc = dsp_get_long_acc(dreg); - //acc++; - //dsp_set_long_acc(dreg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_neg(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_neg"); - - //uint8 areg = (opc >> 8) & 0x1; - - //sint64 acc = dsp_get_long_acc(areg); - //acc = 0 - acc; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_movnp(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_movnp"); - - ErrorLog("dsp_opc_movnp\n"); -} - - -// NEW -void dsp_opc_addax(uint16 opc) -{ - // OutBuffer::AddCode("missing: dsp_opc_addax"); - - uint8 areg = (opc >> 8) & 0x1; - uint8 sreg = (opc >> 9) & 0x1; - - OutBuffer::AddCode("ACC%i += AX%i", areg, sreg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", areg); - - //sint64 ax = dsp_get_long_acx(sreg); - //sint64 acc = dsp_get_long_acc(areg); - //acc += ax; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_addr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_addr"); - - //uint8 areg = (opc >> 8) & 0x1; - //uint8 sreg = ((opc >> 9) & 0x3) + 0x18; - - //sint64 ax = (sint16)g_dsp.r[sreg]; - //ax <<= 16; - - //sint64 acc = dsp_get_long_acc(areg); - //acc += ax; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_subr(uint16 opc) -{ - uint8 areg = (opc >> 8) & 0x1; - uint8 sreg = ((opc >> 9) & 0x3) + 0x18; - - OutBuffer::AddCode("ACC%i -= (sint64)((sint16)%s << 16)", areg, OutBuffer::GetRegName(sreg)); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", areg); - - //sint64 ax = (sint16)g_dsp.r[sreg]; - //ax <<= 16; - - //sint64 acc = dsp_get_long_acc(areg); - //acc -= ax; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - -// NEW -void dsp_opc_subax(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_subax"); - - //int regD = (opc >> 8) & 0x1; - //int regT = (opc >> 9) & 0x1; - - //sint64 Acc = dsp_get_long_acc(regD) - dsp_get_long_acx(regT); - - //dsp_set_long_acc(regD, Acc); - //Update_SR_Register(Acc); -} - -void dsp_opc_addis(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_addis"); - - //uint8 areg = (opc >> 8) & 0x1; - - //sint64 Imm = (sint8)opc; - //Imm <<= 16; - //sint64 acc = dsp_get_long_acc(areg); - //acc += Imm; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_addi(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_addi"); - - //uint8 areg = (opc >> 8) & 0x1; - - //sint64 sub = (sint16)FetchOpcode(); - //sub <<= 16; - //sint64 acc = dsp_get_long_acc(areg); - //acc += sub; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_lsl16(uint16 opc) -{ - uint8 areg = (opc >> 8) & 0x1; - - OutBuffer::AddCode("ACC%i <<= 16", areg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", areg); - - //sint64 acc = dsp_get_long_acc(areg); - //acc <<= 16; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -// NEW -void dsp_opc_madd(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_madd"); - - //uint8 sreg = (opc >> 8) & 0x1; - - //sint64 prod = dsp_get_long_prod(); - //prod += (sint64)dsp_get_ax_l(sreg) * (sint64)dsp_get_ax_h(sreg) * GetMultiplyModifier(); - //dsp_set_long_prod(prod); -} - - -void dsp_opc_lsr16(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_lsr16"); - - //uint8 areg = (opc >> 8) & 0x1; - - //sint64 acc = dsp_get_long_acc(areg); - //acc >>= 16; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -void dsp_opc_asr16(uint16 opc) -{ - uint8 areg = (opc >> 11) & 0x1; - - OutBuffer::AddCode("ACC%i >>= 16", areg); - OutBuffer::AddCode("Update_SR_Register(ACC%i)", areg); - - //sint64 acc = dsp_get_long_acc(areg); - //acc >>= 16; - //dsp_set_long_acc(areg, acc); - - //Update_SR_Register(acc); -} - - -union UOpcodeShifti -{ - uint16 Hex; - struct - { - signed shift : 6; - unsigned negating : 1; - unsigned arithmetic : 1; - unsigned areg : 1; - unsigned op : 7; - }; - struct - { - unsigned ushift : 6; - }; - UOpcodeShifti(uint16 _Hex) - : Hex(_Hex) {} -}; - -void dsp_opc_shifti(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_shifti"); - - //UOpcodeShifti Opcode(opc); - - //// direction: left - //bool ShiftLeft = true; - //uint16 shift = Opcode.ushift; - - //if ((Opcode.negating) && (Opcode.shift < 0)) - //{ - // ShiftLeft = false; - // shift = -Opcode.shift; - //} - - //sint64 acc; - //uint64 uacc; - - //if (Opcode.arithmetic) - //{ - // // arithmetic shift - // uacc = dsp_get_long_acc(Opcode.areg); - - // if (!ShiftLeft) - // { - // uacc >>= shift; - // } - // else - // { - // uacc <<= shift; - // } - - // acc = uacc; - //} - //else - //{ - // acc = dsp_get_long_acc(Opcode.areg); - - // if (!ShiftLeft) - // { - // acc >>= shift; - // } - // else - // { - // acc <<= shift; - // } - //} - - //dsp_set_long_acc(Opcode.areg, acc); - - //Update_SR_Register(acc); -} - - -//------------------------------------------------------------- -// hcs give me this code!! -void dsp_opc_dar(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_dar"); - - //uint8 reg = opc & 0x3; - - //int temp = g_dsp.r[reg] + g_dsp.r[8]; - - //if (temp <= 0x7ff){g_dsp.r[reg] = temp;} - //else {g_dsp.r[reg]--;} -} - - -// hcs give me this code!! -void dsp_opc_iar(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_iar"); - - //uint8 reg = opc & 0x3; - - //int temp = g_dsp.r[reg] + g_dsp.r[8]; - - //if (temp <= 0x7ff){g_dsp.r[reg] = temp;} - //else {g_dsp.r[reg]++;} -} - - -//------------------------------------------------------------- - -void dsp_opc_sbclr(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_sbclr"); - - //uint8 bit = (opc & 0xff) + 6; - //g_dsp.r[R_SR] &= ~(1 << bit); -} - - -void dsp_opc_sbset(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_sbset"); - - //uint8 bit = (opc & 0xff) + 6; - //g_dsp.r[R_SR] |= (1 << bit); -} - - -void dsp_opc_srbith(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_srbith"); - -// switch ((opc >> 8) & 0xf) -// { -// case 0xe: // SET40 -// g_dsp.r[R_SR] &= ~(1 << 14); -// break; -// -///* case 0xf: // SET16 // that doesnt happen on a real console -// g_dsp.r[R_SR] |= (1 << 14); -// break;*/ -// -// default: -// break; -// } -} - - -//------------------------------------------------------------- - -void dsp_opc_movp(uint16 opc) -{ - uint8 dreg = (opc >> 8) & 0x1; - - OutBuffer::AddCode("ACC%i = PROD", dreg); - - //sint64 prod = dsp_get_long_prod(); - //sint64 acc = prod; - //dsp_set_long_acc(dreg, acc); -} - - -void dsp_opc_mul(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mul"); - - //uint8 sreg = (opc >> 11) & 0x1; - //sint64 prod = (sint64)dsp_get_ax_h(sreg) * (sint64)dsp_get_ax_l(sreg) * GetMultiplyModifier(); - - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - -// NEW -void dsp_opc_mulac(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulac"); - - //// add old prod to acc - //uint8 rreg = (opc >> 8) & 0x1; - //sint64 acR = dsp_get_long_acc(rreg) + dsp_get_long_prod(); - //dsp_set_long_acc(rreg, acR); - - //// math new prod - //uint8 sreg = (opc >> 11) & 0x1; - //sint64 prod = dsp_get_ax_l(sreg) * dsp_get_ax_h(sreg) * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -void dsp_opc_mulmv(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulmv"); - - //uint8 rreg = (opc >> 8) & 0x1; - //sint64 prod = dsp_get_long_prod(); - //sint64 acc = prod; - //dsp_set_long_acc(rreg, acc); - - //uint8 areg = ((opc >> 11) & 0x1) + 0x18; - //uint8 breg = ((opc >> 11) & 0x1) + 0x1a; - //sint64 val1 = (sint16)g_dsp.r[areg]; - //sint64 val2 = (sint16)g_dsp.r[breg]; - - //prod = val1 * val2 * GetMultiplyModifier(); - - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_mulmvz(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulmvz"); - - //uint8 sreg = (opc >> 11) & 0x1; - //uint8 rreg = (opc >> 8) & 0x1; - - //// overwrite acc and clear low part - //sint64 prod = dsp_get_long_prod(); - //sint64 acc = prod & ~0xffff; - //dsp_set_long_acc(rreg, acc); - - //// math prod - //prod = (sint64)g_dsp.r[0x18 + sreg] * (sint64)g_dsp.r[0x1a + sreg] * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_mulx(uint16 opc) -{ - // OutBuffer::AddCode("missing: dsp_opc_mulx"); - - uint8 sreg = ((opc >> 12) & 0x1); - uint8 treg = ((opc >> 11) & 0x1); - - OutBuffer::AddCode("PROD = %s * %s * MultiplyModifier", (sreg == 0) ? "AX0_l" : "AX0_h", - (treg == 0) ? "AX1_l" : "AX1_h"); - OutBuffer::AddCode("Update_SR_Register(PROD)"); - - //sint64 val1 = (sreg == 0) ? dsp_get_ax_l(0) : dsp_get_ax_h(0); - //sint64 val2 = (treg == 0) ? dsp_get_ax_l(1) : dsp_get_ax_h(1); - - //sint64 prod = val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_mulxac(uint16 opc) -{ - //// add old prod to acc - uint8 rreg = (opc >> 8) & 0x1; - //sint64 acR = dsp_get_long_acc(rreg) + dsp_get_long_prod(); - //dsp_set_long_acc(rreg, acR); - - OutBuffer::AddCode("ACC%i += PROD", rreg); - - //// math new prod - uint8 sreg = (opc >> 12) & 0x1; - uint8 treg = (opc >> 11) & 0x1; - - OutBuffer::AddCode("PROD = %s * %s * MultiplyModifier", (sreg == 0) ? "AX0_l" : "AX0_h", - (treg == 0) ? "AX1_l" : "AX1_h"); - OutBuffer::AddCode("Update_SR_Register(PROD)"); - - //sint64 val1 = (sreg == 0) ? dsp_get_ax_l(0) : dsp_get_ax_h(0); - //sint64 val2 = (treg == 0) ? dsp_get_ax_l(1) : dsp_get_ax_h(1); - - //sint64 prod = val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_mulxmv(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulxmv"); - - //// add old prod to acc - //uint8 rreg = ((opc >> 8) & 0x1); - //sint64 acR = dsp_get_long_prod(); - //dsp_set_long_acc(rreg, acR); - - //// math new prod - //uint8 sreg = (opc >> 12) & 0x1; - //uint8 treg = (opc >> 11) & 0x1; - - //sint64 val1 = (sreg == 0) ? dsp_get_ax_l(0) : dsp_get_ax_h(0); - //sint64 val2 = (treg == 0) ? dsp_get_ax_l(1) : dsp_get_ax_h(1); - - //sint64 prod = val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_mulxmvz(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_mulxmvz"); - - //// overwrite acc and clear low part - //uint8 rreg = (opc >> 8) & 0x1; - //sint64 prod = dsp_get_long_prod(); - //sint64 acc = prod & ~0xffff; - //dsp_set_long_acc(rreg, acc); - - //// math prod - //uint8 sreg = (opc >> 12) & 0x1; - //uint8 treg = (opc >> 11) & 0x1; - - //sint64 val1 = (sreg == 0) ? dsp_get_ax_l(0) : dsp_get_ax_h(0); - //sint64 val2 = (treg == 0) ? dsp_get_ax_l(1) : dsp_get_ax_h(1); - - //prod = val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); - - //Update_SR_Register(prod); -} - - -// NEW -void dsp_opc_sub(uint16 opc) -{ - uint8 D = (opc >> 8) & 0x1; - - OutBuffer::AddCode("ACC%i -= ACC%i", D, 1-D); - - //sint64 Acc1 = dsp_get_long_acc(D); - //sint64 Acc2 = dsp_get_long_acc(1 - D); - - //Acc1 -= Acc2; - - //dsp_set_long_acc(D, Acc1); -} - - -//------------------------------------------------------------- -// -// --- Table E -// -//------------------------------------------------------------- - -// NEW -void dsp_opc_maddx(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_maddx"); - - //uint8 sreg = (opc >> 9) & 0x1; - //uint8 treg = (opc >> 8) & 0x1; - - //sint64 val1 = (sreg == 0) ? dsp_get_ax_l(0) : dsp_get_ax_h(0); - //sint64 val2 = (treg == 0) ? dsp_get_ax_l(1) : dsp_get_ax_h(1); - - //sint64 prod = dsp_get_long_prod(); - //prod += val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); -} - - -// NEW -void dsp_opc_msubx(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_msubx"); - - //uint8 sreg = (opc >> 9) & 0x1; - //uint8 treg = (opc >> 8) & 0x1; - - //sint64 val1 = (sreg == 0) ? dsp_get_ax_l(0) : dsp_get_ax_h(0); - //sint64 val2 = (treg == 0) ? dsp_get_ax_l(1) : dsp_get_ax_h(1); - - //sint64 prod = dsp_get_long_prod(); - //prod -= val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); -} - - -// NEW -void dsp_opc_maddc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_maddc"); - - //uint sreg = (opc >> 9) & 0x1; - //uint treg = (opc >> 8) & 0x1; - - //sint64 val1 = dsp_get_acc_m(sreg); - //sint64 val2 = dsp_get_ax_h(treg); - - //sint64 prod = dsp_get_long_prod(); - //prod += val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); -} - - -// NEW -void dsp_opc_msubc(uint16 opc) -{ - OutBuffer::AddCode("missing: dsp_opc_msubc"); - - //uint sreg = (opc >> 9) & 0x1; - //uint treg = (opc >> 8) & 0x1; - - //sint64 val1 = dsp_get_acc_m(sreg); - //sint64 val2 = dsp_get_ax_h(treg); - - //sint64 prod = dsp_get_long_prod(); - //prod -= val1 * val2 * GetMultiplyModifier(); - //dsp_set_long_prod(prod); -} - - -//------------------------------------------------------------- -void dsp_op0(uint16 opc) -{ - if (opc == 0) - { - return; - } - - switch ((opc >> 8) & 0xf) - { - case 0x0: - - switch ((opc >> 4) & 0xf) - { - case 0x0: - - switch (opc & 0xf) - { - case 0x4: - case 0x5: - case 0x6: - case 0x7: - dsp_opc_dar(opc); - break; - - case 0x8: - case 0x9: - case 0xa: - case 0xb: - dsp_opc_iar(opc); - break; - - default: - ErrorLog("dsp_op0"); - break; - } - - break; - - case 0x1: - dsp_opc_addarn(opc); - break; - - case 0x2: // HALT - dsp_opc_halt(opc); - break; - - case 0x4: // LOOP - case 0x5: // LOOP - dsp_opc_loop(opc); - break; - - case 0x6: // BLOOP - case 0x7: // BLOOP - dsp_opc_bloop(opc); - break; - - case 0x8: // LRI - case 0x9: // LRI - dsp_opc_lri(opc); - break; - - case 0xC: // LR - case 0xD: // LR - dsp_opc_lr(opc); - break; - - case 0xE: // SR - case 0xF: // SR - dsp_opc_sr(opc); - break; - - default: - ErrorLog("dsp_op0"); - break; - } - - break; - - case 0x2: - - switch ((opc >> 4) & 0xf) - { - case 0x0: // ADDI - dsp_opc_addi(opc); - break; - - case 0x1: // IL - dsp_opc_ilrr(opc); - break; - - case 0x2: // XORI - dsp_opc_xori(opc); - break; - - case 0x4: // ANDI - dsp_opc_andi(opc); - break; - - case 0x6: // ORI - dsp_opc_ori(opc); - break; - - case 0x7: // - dsp_opc_ifcc(opc); - break; - - case 0x8: // SUBF - dsp_opc_subf(opc); - break; - - case 0x9: // Jxx - dsp_opc_jcc(opc); - break; - - case 0xa: // ANDF - dsp_opc_andf(opc); - break; - - case 0xb: // CALL - dsp_opc_call(opc); - break; - - case 0xc: - dsp_opc_andfc(opc); - break; - - case 0xd: // RET - dsp_opc_ret(opc); - break; - - case 0xf: // RTI - dsp_opc_rti(opc); - break; - - default: - ErrorLog("dsp_op0"); - break; - } - - break; - - case 0x3: - - switch ((opc >> 4) & 0xf) - { - case 0x0: // ADDAI - dsp_opc_addi(opc); - break; - - case 0x1: // ILR - dsp_opc_ilrr(opc); - break; - - case 0x2: // XORI - dsp_opc_xori(opc); - break; - - case 0x4: // ANDI - dsp_opc_andi(opc); - break; - - case 0x6: // ORI - dsp_opc_ori(opc); - break; - - case 0x8: // SUBF - dsp_opc_subf(opc); - break; - - case 0xa: // ANDF - dsp_opc_andf(opc); - break; - - case 0xc: // ANDFC - dsp_opc_andfc(opc); - break; - - default: - ErrorLog("dsp_op0"); - break; - } - - break; - - case 0x4: - case 0x5: - dsp_opc_addis(opc); - break; - - case 0x6: // SUBISF - case 0x7: - dsp_opc_cmpis(opc); - break; - - case 0x8: // LRIS - case 0x9: - case 0xa: - case 0xb: - case 0xc: - case 0xd: - case 0xe: - case 0xf: - dsp_opc_lris(opc); - break; - - default: - ErrorLog("dsp_op0"); - break; - } -} - - -void dsp_op1(uint16 opc) -{ - switch ((opc >> 8) & 0xf) - { - case 0x0: - dsp_opc_loopi(opc); - break; - - case 0x1: // BLOOPI - dsp_opc_bloopi(opc); - break; - - case 0x2: // SBCLR - dsp_opc_sbclr(opc); - break; - - case 0x3: // SBSET - dsp_opc_sbset(opc); - break; - - case 0x4: // shifti - case 0x5: - dsp_opc_shifti(opc); - break; - - case 0x6: // SI - dsp_opc_si(opc); - break; - - case 0x7: // JMPA/CALLA - dsp_opc_jmpa(opc); - break; - - case 0x8: // LRRx - case 0x9: // LRRx - dsp_opc_lrr(opc); - break; - - case 0xa: // SRRx - case 0xb: // SRRx - dsp_opc_srr(opc); - break; - - case 0xc: // MRR - case 0xd: // MRR - case 0xe: // MRR - case 0xf: // MRR - dsp_opc_mrr(opc); - break; - - default: - ErrorLog("dsp_op1"); - break; - } -} - - -void dsp_op2(uint16 opc) -{ - // lrs, srs - uint8 reg = ((opc >> 8) & 0x7) + 0x18; - uint16 addr = (sint8) opc; - - if (opc & 0x0800) - { - OutBuffer::AddCode("WriteDMEM(%s, %s)", OutBuffer::GetMemName(addr), OutBuffer::GetRegName(reg)); - - // srs - // dsp_dmem_write(addr, g_dsp.r[reg]); - } - else - { - OutBuffer::AddCode("%s = ReadDMEM(%s)", OutBuffer::GetRegName(reg), OutBuffer::GetMemName(addr)); - - // lrs - // g_dsp.r[reg] = dsp_dmem_read(addr); - } -} - - -void dsp_op3(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x0: - case 0x1: - case 0x2: - case 0x3: - dsp_opc_xorr(opc); - break; - - case 0x4: - case 0x5: - case 0x6: - case 0x7: - dsp_opc_andr(opc); - break; - - case 0x8: - case 0x9: - case 0xa: - case 0xb: - dsp_opc_orr(opc); - break; - - case 0xc: - case 0xd: - dsp_opc_andc(opc); - break; - - default: - ErrorLog("dsp_op3"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_op4(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x0: - case 0x1: - case 0x2: - case 0x3: - case 0x4: - case 0x5: - case 0x6: - case 0x7: - dsp_opc_addr(opc); - break; - - case 0x8: - case 0x9: - case 0xa: - case 0xb: - dsp_opc_addax(opc); - break; - - case 0xc: - case 0xd: - dsp_opc_add(opc); - break; - - case 0xe: - case 0xf: - dsp_opc_addp(opc); - break; - - default: - ErrorLog("dsp_op4"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_op5(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x0: - case 0x1: - case 0x2: - case 0x3: - case 0x4: - case 0x5: - case 0x6: - case 0x7: - dsp_opc_subr(opc); - break; - - case 0x8: - case 0x9: - case 0xa: - case 0xb: - dsp_opc_subax(opc); - break; - - case 0xc: - case 0xd: - dsp_opc_sub(opc); - break; - - default: - ErrorLog("dsp_op5: %x", (opc >> 8) & 0xf); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_op6(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x00: // MOVR - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - dsp_opc_movr(opc); - break; - - case 0x8: // MVAXA - case 0x9: - case 0xa: - case 0xb: - dsp_opc_movax(opc); - break; - - case 0xe: - case 0xf: - dsp_opc_movp(opc); - break; - - default: - ErrorLog("dsp_op6"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_op7(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x0: - case 0x1: - case 0x2: - case 0x3: - dsp_opc_addaxl(opc); - break; - - case 0x4: - case 0x5: - dsp_opc_incm(opc); - break; - - case 0x6: - case 0x7: - dsp_opc_inc(opc); - break; - - case 0x8: - case 0x9: - dsp_opc_decm(opc); - break; - - case 0xa: - case 0xb: - dsp_opc_dec(opc); - break; - - case 0xc: - case 0xd: - dsp_opc_neg(opc); - break; - - case 0xe: - case 0xf: - dsp_opc_movnp(opc); - break; - - default: - ErrorLog("dsp_op7"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_op8(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x0: - case 0x8: - dsp_opc_nx(opc); - break; - - case 0x1: // CLR 0 - case 0x9: // CLR 1 - dsp_opc_clr(opc); - break; - - case 0x2: // CMP - dsp_opc_cmp(opc); - break; - - case 0x4: // CLRP - dsp_opc_clrp(opc); - break; - - case 0x6: - case 0x7: - dsp_opc_tstaxh(opc); - break; - - case 0xc: - case 0xb: - case 0xe: // SET40 - case 0xd: - case 0xa: - case 0xf: - dsp_opc_srbith(opc); - break; - - default: - ErrorLog("dsp_op8"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_op9(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x02: - case 0x03: - case 0x0a: - case 0x0b: - dsp_opc_mulmvz(opc); - break; - - case 0x04: - case 0x05: - case 0x0c: - case 0x0d: - dsp_opc_mulac(opc); - break; - - case 0x6: - case 0x7: - case 0xe: - case 0xf: - dsp_opc_mulmv(opc); - break; - - case 0x0: - case 0x8: - dsp_opc_mul(opc); - break; - - case 0x1: - case 0x9: - dsp_opc_asr16(opc); - break; - - default: - ErrorLog("dsp_op9"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_opab(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0x7) - { - case 0x0: - dsp_opc_mulx(opc); - break; - - case 0x1: - dsp_opc_tsta(opc); - break; - - case 0x2: - case 0x3: - dsp_opc_mulxmvz(opc); - break; - - case 0x4: - case 0x5: - dsp_opc_mulxac(opc); - break; - - case 0x6: - case 0x7: - dsp_opc_mulxmv(opc); - break; - - default: - ErrorLog("dsp_opab"); - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_opcd(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0x7) - { - case 0x0: // MULC - dsp_opc_mulc(opc); - break; - - case 0x1: // CMPAR - dsp_opc_cmpar(opc); - break; - - case 0x2: // MULCMVZ - case 0x3: - dsp_opc_mulcmvz(opc); - break; - - case 0x4: // MULCAC - case 0x5: - dsp_opc_mulcac(opc); - break; - - case 0x6: // MULCMV - case 0x7: - dsp_opc_mulcmv(opc); - break; - - default: - ErrorLog("dsp_opcd"); - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_ope(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 10) & 0x3) - { - case 0x00: // MADDX - dsp_opc_maddx(opc); - break; - - case 0x01: // MSUBX - dsp_opc_msubx(opc); - break; - - case 0x02: // MADDC - dsp_opc_maddc(opc); - break; - - case 0x03: // MSUBC - dsp_opc_msubc(opc); - break; - - default: - ErrorLog("dsp_ope"); - } - - dsp_op_ext_ops_epi(opc); -} - - -void dsp_opf(uint16 opc) -{ - dsp_op_ext_ops_pro(opc); - - switch ((opc >> 8) & 0xf) - { - case 0x0: - case 0x1: - dsp_opc_lsl16(opc); - break; - - case 0x02: - case 0x03: - dsp_opc_madd(opc); - break; - - case 0x4: - case 0x5: - dsp_opc_lsr16(opc); - break; - - case 0x8: - case 0x9: - case 0xa: - case 0xb: - dsp_opc_addpaxz(opc); - break; - - case 0xe: - case 0xf: - dsp_opc_movpz(opc); - break; - - default: - ErrorLog("dsp_opf"); - break; - } - - dsp_op_ext_ops_epi(opc); -} - - diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.h b/docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.h deleted file mode 100644 index 32627b5e0e4d..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSPOpcodes.h +++ /dev/null @@ -1,48 +0,0 @@ -/*==================================================================== - - filename: gdsp_opcodes.h - project: GCemu - created: 2004-6-18 - mail: duddie@walla.com - - Copyright (c) 2005 Duddie & Tratax - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - ====================================================================*/ -#ifndef _GDSP_OPCODES_H -#define _GDSP_OPCODES_H - -void dsp_op0(uint16 opc); -void dsp_op1(uint16 opc); -void dsp_op2(uint16 opc); -void dsp_op3(uint16 opc); -void dsp_op4(uint16 opc); -void dsp_op5(uint16 opc); -void dsp_op6(uint16 opc); -void dsp_op7(uint16 opc); -void dsp_op8(uint16 opc); -void dsp_op9(uint16 opc); -void dsp_opab(uint16 opc); -void dsp_opcd(uint16 opc); -void dsp_ope(uint16 opc); -void dsp_opf(uint16 opc); - - -#define R_SR 0x13 - -#define FLAG_ENABLE_INTERUPT 11 - -#endif diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.cpp b/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.cpp deleted file mode 100644 index aeddd410b53e..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// DSP_InterC.cpp : Defines the entry point for the console application. -// - -#include "stdafx.h" - -#include "DSPOpcodes.h" - -uint16 g_IMemory[0x1000]; -uint16 g_currentAddress; - -uint16 FetchOpcode() -{ - uint16 value = swap16(g_IMemory[g_currentAddress & 0x0FFF]); - g_currentAddress++; - return value; -} - -void DecodeOpcode(uint16 op); - -void Decode(uint16 startAddress, uint16 endAddress) -{ - g_currentAddress = startAddress; - - while (g_currentAddress < endAddress) - { - uint16 oldPC = g_currentAddress; - uint16 op = FetchOpcode(); - - OutBuffer::Add("// l_%4X:", oldPC); - DecodeOpcode(op); - } -} - - -int _tmain(int argc, _TCHAR* argv[]) -{ - FILE* pFile = fopen("c:\\_\\dsp_rom.bin", "rb"); - if (pFile == nullptr) - return -1; - - fread(g_IMemory, 0x1000, 1, pFile); - fclose(pFile); - - - ////// - OutBuffer::Init(); - Decode(0x80e7, 0x81f9); - - - return 0; -} - diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj b/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj deleted file mode 100644 index 177b25d56000..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj +++ /dev/null @@ -1,112 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - - {A010425E-9D5E-461E-910D-0804C2A944D5} - DSP_InterC - Win32Proj - - - - Application - Unicode - true - - - Application - Unicode - - - - - - - - - - - - - <_ProjectFileVersion>10.0.30319.1 - $(SolutionDir)$(Configuration)\ - $(Configuration)\ - true - $(SolutionDir)$(Configuration)\ - $(Configuration)\ - false - AllRules.ruleset - - - AllRules.ruleset - - - - - - Disabled - WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - EnableFastChecks - MultiThreadedDebugDLL - Use - Level3 - EditAndContinue - - - true - Console - MachineX86 - - - - - MaxSpeed - true - WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) - MultiThreadedDLL - true - Use - Level3 - ProgramDatabase - - - true - Console - true - true - MachineX86 - - - - - - - - - Create - Create - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj.filters b/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj.filters deleted file mode 100644 index 319d625db3e4..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/DSP_InterC.vcxproj.filters +++ /dev/null @@ -1,54 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - Source Files - - - Source Files - - - Source Files - - - Header Files - - - Header Files - - - - - - \ No newline at end of file diff --git a/docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.cpp b/docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.cpp deleted file mode 100644 index 005fcc2d10b7..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/OutBuffer.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// stdafx.cpp : source file that includes just the standard includes -// DSP_InterC.pch will be the pre-compiled header -// stdafx.obj will contain the pre-compiled type information - -#include "stdafx.h" - -#include -#include - -namespace OutBuffer -{ - void Init() - { - - } - - void Add(const char* _fmt, ...) - { - static char Msg[2048]; - va_list ap; - - va_start(ap, _fmt); - vsprintf(Msg, _fmt, ap); - va_end(ap); - - printf("%s\n", Msg); - } - - void AddCode(const char* _fmt, ...) - { - static char Msg[2048]; - va_list ap; - - va_start(ap, _fmt); - vsprintf(Msg, _fmt, ap); - va_end(ap); - - printf(" %s;\n", Msg); - } - - // predefined labels - typedef struct pdlabel_t - { - uint16 addr; - const char* name; - const char* description; - } pdlabels_t; - - pdlabel_t regnames[] = - { - {0x00, "R00", "Register 00",}, - {0x01, "R01", "Register 01",}, - {0x02, "R02", "Register 02",}, - {0x03, "R03", "Register 03",}, - {0x04, "R04", "Register 04",}, - {0x05, "R05", "Register 05",}, - {0x06, "R06", "Register 06",}, - {0x07, "R07", "Register 07",}, - {0x08, "R08", "Register 08",}, - {0x09, "R09", "Register 09",}, - {0x0a, "R10", "Register 10",}, - {0x0b, "R11", "Register 11",}, - {0x0c, "ST0", "Call stack",}, - {0x0d, "ST1", "Data stack",}, - {0x0e, "ST2", "Loop address stack",}, - {0x0f, "ST3", "Loop counter",}, - {0x00, "ACH0", "Accumulator High 0",}, - {0x11, "ACH1", "Accumulator High 1",}, - {0x12, "CR", "Config Register",}, - {0x13, "SR", "Special Register",}, - {0x14, "PROD_l", "PROD L",}, - {0x15, "PROD_m1", "PROD M1",}, - {0x16, "PROD_h", "PROD H",}, - {0x17, "PROD_m2", "PROD M2",}, - {0x18, "AX0_l", "Additional Accumulators Low 0",}, - {0x19, "AX1_l", "Additional Accumulators Low 1",}, - {0x1a, "AX0_h", "Additional Accumulators High 0",}, - {0x1b, "AX1_h", "Additional Accumulators High 1",}, - {0x1c, "AC0_l", "Register 28",}, - {0x1d, "AC1_l", "Register 29",}, - {0x1e, "AC0_m", "Register 00",}, - {0x1f, "AC1_m", "Register 00",}, - - // additional to resolve special names - {0x20, "ACC0", "Accumulators 0",}, - {0x21, "ACC1", "Accumulators 1",}, - {0x22, "AX0", "Additional Accumulators 0",}, - {0x23, "AX1", "Additional Accumulators 1",}, - }; - - const pdlabel_t pdlabels[] = - { - {0xffa0, "COEF_A1_0", "COEF_A1_0",}, - {0xffa1, "COEF_A2_0", "COEF_A2_0",}, - {0xffa2, "COEF_A1_1", "COEF_A1_1",}, - {0xffa3, "COEF_A2_1", "COEF_A2_1",}, - {0xffa4, "COEF_A1_2", "COEF_A1_2",}, - {0xffa5, "COEF_A2_2", "COEF_A2_2",}, - {0xffa6, "COEF_A1_3", "COEF_A1_3",}, - {0xffa7, "COEF_A2_3", "COEF_A2_3",}, - {0xffa8, "COEF_A1_4", "COEF_A1_4",}, - {0xffa9, "COEF_A2_4", "COEF_A2_4",}, - {0xffaa, "COEF_A1_5", "COEF_A1_5",}, - {0xffab, "COEF_A2_5", "COEF_A2_5",}, - {0xffac, "COEF_A1_6", "COEF_A1_6",}, - {0xffad, "COEF_A2_6", "COEF_A2_6",}, - {0xffae, "COEF_A1_7", "COEF_A1_7",}, - {0xffaf, "COEF_A2_7", "COEF_A2_7",}, - {0xffc9, "DSCR", "DSP DMA Control Reg",}, - {0xffcb, "DSBL", "DSP DMA Block Length",}, - {0xffcd, "DSPA", "DSP DMA DMEM Address",}, - {0xffce, "DSMAH", "DSP DMA Mem Address H",}, - {0xffcf, "DSMAL", "DSP DMA Mem Address L",}, - {0xffd1, "SampleFormat", "SampleFormat",}, - - {0xffd3, "Unk Zelda", "Unk Zelda writes to it",}, - - {0xffd4, "ACSAH", "Accelerator start address H",}, - {0xffd5, "ACSAL", "Accelerator start address L",}, - {0xffd6, "ACEAH", "Accelerator end address H",}, - {0xffd7, "ACEAL", "Accelerator end address L",}, - {0xffd8, "ACCAH", "Accelerator current address H",}, - {0xffd9, "ACCAL", "Accelerator current address L",}, - {0xffda, "pred_scale", "pred_scale",}, - {0xffdb, "yn1", "yn1",}, - {0xffdc, "yn2", "yn2",}, - {0xffdd, "ARAM", "Direct Read from ARAM (uses ADPCM)",}, - {0xffde, "GAIN", "Gain",}, - {0xffef, "AMDM", "ARAM DMA Request Mask",}, - {0xfffb, "DIRQ", "DSP IRQ Request",}, - {0xfffc, "DMBH", "DSP Mailbox H",}, - {0xfffd, "DMBL", "DSP Mailbox L",}, - {0xfffe, "CMBH", "CPU Mailbox H",}, - {0xffff, "CMBL", "CPU Mailbox L",}, - }; - - const char* GetRegName(uint16 reg) - { - return regnames[reg].name; - } - - const char* GetMemName(uint16 addr) - { - static char Buffer[1024]; - for (int i=0; i 0); -} - - -// --------------------------------------------------------------------------------------- -// -// --- reg -// -// --------------------------------------------------------------------------------------- - -inline uint16 dsp_op_read_reg(uint8 reg) -{ - uint16 val; - - switch (reg & 0x1f) - { - case 0x0c: - case 0x0d: - case 0x0e: - case 0x0f: - val = dsp_reg_load_stack(reg - 0x0c); - break; - - default: - val = g_dsp.r[reg]; - break; - } - - return(val); -} - - -inline void dsp_op_write_reg(uint8 reg, uint16 val) -{ - switch (reg & 0x1f) - { - case 0x0c: - case 0x0d: - case 0x0e: - case 0x0f: - dsp_reg_store_stack(reg - 0x0c, val); - break; - - default: - g_dsp.r[reg] = val; - break; - } -} - - -// --------------------------------------------------------------------------------------- -// -// --- prod -// -// --------------------------------------------------------------------------------------- - - -inline sint64 dsp_get_long_prod() -{ - sint64 val; - sint64 low_prod; - val = (sint8)g_dsp.r[0x16]; - val <<= 32; - low_prod = g_dsp.r[0x15]; - low_prod += g_dsp.r[0x17]; - low_prod <<= 16; - low_prod |= g_dsp.r[0x14]; - val += low_prod; - return(val); -} - - -inline void dsp_set_long_prod(sint64 val) -{ - g_dsp.r[0x14] = (uint16)val; - val >>= 16; - g_dsp.r[0x15] = (uint16)val; - val >>= 16; - g_dsp.r[0x16] = (uint16)val; - g_dsp.r[0x17] = 0; -} - - -// --------------------------------------------------------------------------------------- -// -// --- acc -// -// --------------------------------------------------------------------------------------- - -inline sint64 dsp_get_long_acc(uint8 reg) -{ - _dbg_assert_(reg < 2); - sint64 val; - sint64 low_acc; - val = (sint8)g_dsp.r[0x10 + reg]; - val <<= 32; - low_acc = g_dsp.r[0x1e + reg]; - low_acc <<= 16; - low_acc |= g_dsp.r[0x1c + reg]; - val |= low_acc; - return(val); -} - - -inline uint64 dsp_get_ulong_acc(uint8 reg) -{ - _dbg_assert_(reg < 2); - uint64 val; - uint64 low_acc; - val = (uint8)g_dsp.r[0x10 + reg]; - val <<= 32; - low_acc = g_dsp.r[0x1e + reg]; - low_acc <<= 16; - low_acc |= g_dsp.r[0x1c + reg]; - val |= low_acc; - return(val); -} - - -inline void dsp_set_long_acc(uint8 _reg, sint64 val) -{ - _dbg_assert_(_reg < 2); - g_dsp.r[0x1c + _reg] = (uint16)val; - val >>= 16; - g_dsp.r[0x1e + _reg] = (uint16)val; - val >>= 16; - g_dsp.r[0x10 + _reg] = (uint16)val; -} - - -inline sint16 dsp_get_acc_l(uint8 _reg) -{ - _dbg_assert_(_reg < 2); - return(g_dsp.r[0x1c + _reg]); -} - - -inline sint16 dsp_get_acc_m(uint8 _reg) -{ - _dbg_assert_(_reg < 2); - return(g_dsp.r[0x1e + _reg]); -} - - -inline sint16 dsp_get_acc_h(uint8 _reg) -{ - _dbg_assert_(_reg < 2); - return(g_dsp.r[0x10 + _reg]); -} - - -// --------------------------------------------------------------------------------------- -// -// --- acx -// -// --------------------------------------------------------------------------------------- - - -inline sint64 dsp_get_long_acx(uint8 _reg) -{ - _dbg_assert_(_reg < 2); - sint64 val = (sint16)g_dsp.r[0x1a + _reg]; - val <<= 16; - sint64 low_acc = g_dsp.r[0x18 + _reg]; - val |= low_acc; - return(val); -} - - -inline sint16 dsp_get_ax_l(uint8 _reg) -{ - _dbg_assert_(_reg < 2); - return(g_dsp.r[0x18 + _reg]); -} - - -inline sint16 dsp_get_ax_h(uint8 _reg) -{ - _dbg_assert_(_reg < 2); - return(g_dsp.r[0x1a + _reg]); -} - - -#endif diff --git a/docs/DSP/DSP_InterC/DSP_InterC/stdafx.cpp b/docs/DSP/DSP_InterC/DSP_InterC/stdafx.cpp deleted file mode 100644 index a90ebd1f3636..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/stdafx.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// stdafx.cpp : source file that includes just the standard includes -// DSP_InterC.pch will be the pre-compiled header -// stdafx.obj will contain the pre-compiled type information - -#include "stdafx.h" - -// TODO: reference any additional headers you need in STDAFX.H -// and not in this file - -#include - -void ErrorLog(const char* _fmt, ...) -{ - char Msg[512]; - va_list ap; - - va_start(ap, _fmt); - vsprintf(Msg, _fmt, ap); - va_end(ap); - - printf("Error"); - -} \ No newline at end of file diff --git a/docs/DSP/DSP_InterC/DSP_InterC/stdafx.h b/docs/DSP/DSP_InterC/DSP_InterC/stdafx.h deleted file mode 100644 index 6948aa4ad3fe..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/stdafx.h +++ /dev/null @@ -1,39 +0,0 @@ -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - -#pragma once - -#include "targetver.h" - -#include -#include - - - - - - -typedef unsigned char uint8; -typedef unsigned short uint16; -typedef unsigned int uint32; -typedef unsigned long long uint64; -typedef unsigned int uint; - -typedef signed char sint8; -typedef signed short sint16; -typedef signed int sint32; -typedef signed long long sint64; - -extern uint16 FetchOpcode(); -extern void ErrorLog(const char* _fmt, ...); - -inline uint16 swap16(uint16 x) -{ - return((x >> 8) | (x << 8)); -} - -#include "OutBuffer.h" - -// TODO: reference additional headers your program requires here diff --git a/docs/DSP/DSP_InterC/DSP_InterC/targetver.h b/docs/DSP/DSP_InterC/DSP_InterC/targetver.h deleted file mode 100644 index 6fe8eb79e1a1..000000000000 --- a/docs/DSP/DSP_InterC/DSP_InterC/targetver.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -// The following macros define the minimum required platform. The minimum required platform -// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run -// your application. The macros work by enabling all features available on platform versions up to and -// including the version specified. - -// Modify the following defines if you have to target a platform prior to the ones specified below. -// Refer to MSDN for the latest info on corresponding values for different platforms. -#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista. -#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows. -#endif - From 9be878cd28dca24b445e5446ce79e7383a000ff4 Mon Sep 17 00:00:00 2001 From: Sintendo <3380580+Sintendo@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:53:27 +0100 Subject: [PATCH 015/270] JitArm64_Integer: subfzex - Optimize a == 0 with in-host carry Before: 0x52800019 mov w25, #0x0 ; =0 0x7a1903f9 ngcs w25, w25 0x1a9f37f5 cset w21, hs 0x390bd3b5 strb w21, [x29, #0x2f4] After: 0x5a9f23f9 csetm w25, lo --- Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index e82db407563d..3cf4175b72d6 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1326,6 +1326,15 @@ void JitArm64::subfzex(UGeckoInstruction inst) gpr.SetImmediate(d, imm + carry); ComputeCarry(Interpreter::Helper_Carry(imm, carry)); } + else if (gpr.IsImm(a, 0) && js.carryFlag == CarryFlag::InHostCarry) + { + gpr.BindToRegister(d, false); + // RD = ~0 + carry = -1 + carry = carry ? 0 : -1 + // Note that CSETM sets the destination to -1 if the condition is true, + // and 0 otherwise. Hence, the condition must be carry clear. + CSETM(gpr.R(d), CC_CC); + // Carry remains unchanged. + } else { gpr.BindToRegister(d, d == a); From 21c26052989f6f8c29c6670c40f35df84638d12b Mon Sep 17 00:00:00 2001 From: Sintendo <3380580+Sintendo@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:54:56 +0100 Subject: [PATCH 016/270] JitArm64_Integer: subfzex - Optimize a == -1 with in-host carry Before: 0x12800019 mov w25, #-0x1 ; =-1 0x7a1903fa ngcs w26, w25 0x1a9f37f8 cset w24, hs 0x390bd3b8 strb w24, [x29, #0x2f4] After: 0x1a9f37fa cset w26, hs --- Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp index 3cf4175b72d6..d60a499594c3 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Integer.cpp @@ -1335,6 +1335,13 @@ void JitArm64::subfzex(UGeckoInstruction inst) CSETM(gpr.R(d), CC_CC); // Carry remains unchanged. } + else if (gpr.IsImm(a, 0xFFFFFFFF) && js.carryFlag == CarryFlag::InHostCarry) + { + gpr.BindToRegister(d, false); + // RD = ~(-1) + carry = 0 + carry = carry ? 1 : 0 + CSET(gpr.R(d), CC_CS); + ComputeCarry(false); + } else { gpr.BindToRegister(d, d == a); From 9fd77c038ffbcee435f829a2fa9134626cfc7fa5 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Fri, 10 Apr 2026 13:08:40 -0500 Subject: [PATCH 017/270] GameSettings: Don't force immediate XFB off in Ultimate Spider-Man. --- Data/Sys/GameSettings/GUT.ini | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Data/Sys/GameSettings/GUT.ini b/Data/Sys/GameSettings/GUT.ini index 32346839582f..6e50fa119e1d 100644 --- a/Data/Sys/GameSettings/GUT.ini +++ b/Data/Sys/GameSettings/GUT.ini @@ -1,17 +1,7 @@ # GUTD52, GUTE52, GUTF52, GUTI52, GUTJC0, GUTP52, GUTS52 - Ultimate Spider-Man [Core] -# Values set here will override the main Dolphin settings. MMU = True -[OnFrame] -# Add memory patches to be applied every frame here. - -[ActionReplay] -# Add action replay cheats here. - [Video_Hacks] -ImmediateXFBEnable = False BBoxEnable = True - -[Video_Settings] From cb9f8a2b04c26e063597d09e1cb7f65c79359857 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 11 Apr 2026 17:55:42 -0500 Subject: [PATCH 018/270] GameSettings: Disable fast depth calc to fix missing text and allow immediate XFB in Def Jam: Fight for NY. --- Data/Sys/GameSettings/GNW.ini | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Data/Sys/GameSettings/GNW.ini b/Data/Sys/GameSettings/GNW.ini index e53570b3e3d2..2f5b8125b64d 100644 --- a/Data/Sys/GameSettings/GNW.ini +++ b/Data/Sys/GameSettings/GNW.ini @@ -1,10 +1,5 @@ -# GNWE69, GNWP69 - Def Jam Fight For NY +# GNWE69, GNWP69 - Def Jam: Fight for NY -[Core] -# Values set here will override the main Dolphin settings. - -[OnFrame] -# Add memory patches to be applied every frame here. - -[Video_Hacks] -ImmediateXFBEnable = False +[Video_Settings] +# Avoids missing text in menus. +FastDepthCalc = False From dd049f7ed2515c91c78e9db9b955dd2dfeba588a Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 12 Apr 2026 21:21:57 -0500 Subject: [PATCH 019/270] GameSettings: Add CPU overclock to work around long black screen on boot in "Deadly Creatures". An "Overclock" value of at least 1.06 seems to currently work around this issue. --- Data/Sys/GameSettings/RDC.ini | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Data/Sys/GameSettings/RDC.ini diff --git a/Data/Sys/GameSettings/RDC.ini b/Data/Sys/GameSettings/RDC.ini new file mode 100644 index 000000000000..41779a798d16 --- /dev/null +++ b/Data/Sys/GameSettings/RDC.ini @@ -0,0 +1,6 @@ +# RDCE78, RDCP78 - Deadly Creatures + +[Core] +# Overclock to work around long black screen on boot. +Overclock = 1.10 +OverclockEnable = True From 908ddf359337b858cb2dc7a411c4845b9b8fc890 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 13 Apr 2026 12:18:03 -0700 Subject: [PATCH 020/270] HacksWidget: Move function declarations to match convention Move function declarations before variable declarations. --- .../Core/DolphinQt/Config/Graphics/HacksWidget.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h index 58504e791e83..2148c41871b0 100644 --- a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h +++ b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h @@ -22,6 +22,14 @@ class HacksWidget final : public QWidget explicit HacksWidget(GraphicsPane* gfx_pane); private: + void CreateWidgets(); + void ConnectWidgets(); + void AddDescriptions(); + + void UpdateGPUTextureDecodingEnabled(); + void UpdateDeferEFBCopiesEnabled(); + void UpdateSkipPresentingDuplicateFramesEnabled(); + void OnBackendChanged(const QString& backend_name); // EFB @@ -48,12 +56,4 @@ class HacksWidget final : public QWidget ConfigBool* m_save_texture_cache_state; Config::Layer* m_game_layer = nullptr; - - void CreateWidgets(); - void ConnectWidgets(); - void AddDescriptions(); - - void UpdateGPUTextureDecodingEnabled(); - void UpdateDeferEFBCopiesEnabled(); - void UpdateSkipPresentingDuplicateFramesEnabled(); }; From 158b8cfd7c0a3112d1f0860ba3147817964938f1 Mon Sep 17 00:00:00 2001 From: TryTwo Date: Mon, 13 Apr 2026 23:46:44 -0700 Subject: [PATCH 021/270] Debugger: Register Widget bug fix. Block updates when changing column data type. --- Source/Core/DolphinQt/Debugger/RegisterWidget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Core/DolphinQt/Debugger/RegisterWidget.cpp b/Source/Core/DolphinQt/Debugger/RegisterWidget.cpp index 84b7e7b74f67..249a484e3c3d 100644 --- a/Source/Core/DolphinQt/Debugger/RegisterWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/RegisterWidget.cpp @@ -283,12 +283,14 @@ void RegisterWidget::ShowContextMenu() view_double_column}) { connect(action, &QAction::triggered, [this, action] { + m_updating = true; auto col = m_table->currentItem()->column(); for (int i = 0; i < 32; i++) { auto* update_item = static_cast(m_table->item(i, col)); update_item->SetDisplay(static_cast(action->data().toInt())); } + m_updating = false; }); } From b35223e0f361cafef844913cecd9fbe01061539b Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 13 Apr 2026 13:23:12 -0700 Subject: [PATCH 022/270] HacksWidget: Merge GPU Texture Decoding disabled tooltip with BalloonTip When GPU Texture Decoding is disabled, explain why in the BalloonTip instead of spawning a separate tooltip. --- .../DolphinQt/Config/Graphics/HacksWidget.cpp | 57 +++++++++++++------ .../DolphinQt/Config/Graphics/HacksWidget.h | 3 +- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp index 66ca11f6af75..274c789c9082 100644 --- a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp +++ b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp @@ -25,10 +25,13 @@ HacksWidget::HacksWidget(GraphicsPane* gfx_pane) : m_game_layer{gfx_pane->GetCon ConnectWidgets(); AddDescriptions(); + const auto get_backend_name = []() { return tr(Config::Get(Config::MAIN_GFX_BACKEND).data()); }; + connect(gfx_pane, &GraphicsPane::BackendChanged, this, &HacksWidget::OnBackendChanged); - OnBackendChanged(QString::fromStdString(Config::Get(Config::MAIN_GFX_BACKEND))); connect(gfx_pane, &GraphicsPane::UpdateGPUTextureDecoding, this, - &HacksWidget::UpdateGPUTextureDecodingEnabled); + [this, get_backend_name] { UpdateGPUTextureDecodingEnabled(get_backend_name()); }); + + OnBackendChanged(get_backend_name()); } void HacksWidget::CreateWidgets() @@ -126,16 +129,14 @@ void HacksWidget::CreateWidgets() void HacksWidget::OnBackendChanged(const QString& backend_name) { const bool bbox = g_backend_info.bSupportsBBox; - const bool gpu_texture_decoding = g_backend_info.bSupportsGPUTextureDecoding; - - UpdateGPUTextureDecodingEnabled(); m_disable_bounding_box->setEnabled(bbox); const QString tooltip = tr("%1 doesn't support this feature on your system.") .arg(tr(backend_name.toStdString().c_str())); - m_gpu_texture_decoding->setToolTip(!gpu_texture_decoding ? tooltip : QString{}); m_disable_bounding_box->setToolTip(!bbox ? tooltip : QString{}); + + UpdateGPUTextureDecodingEnabled(backend_name); } void HacksWidget::ConnectWidgets() @@ -211,12 +212,6 @@ void HacksWidget::AddDescriptions() "

This setting is unavailable when Immediately Present XFB or VBI Skip is " "enabled. In those cases, duplicate frames are never presented." "

If unsure, leave this checked."); - static const char TR_GPU_DECODING_DESCRIPTION[] = QT_TR_NOOP( - "Enables texture decoding using the GPU instead of the CPU.

This may result in " - "performance gains in some scenarios, or on systems where the CPU is the " - "bottleneck.

This setting is disabled when Arbitrary Mipmap Detection is " - "enabled.

" - "If unsure, leave this unchecked."); static const char TR_FAST_DEPTH_CALC_DESCRIPTION[] = QT_TR_NOOP( "Uses a less accurate algorithm to calculate depth values.

Causes issues in a few " "games, but can result in a decent speed increase depending on the game and/or " @@ -253,7 +248,6 @@ void HacksWidget::AddDescriptions() m_store_xfb_copies->SetDescription(tr(TR_STORE_XFB_TO_TEXTURE_DESCRIPTION)); m_immediate_xfb->SetDescription(tr(TR_IMMEDIATE_XFB_DESCRIPTION)); m_skip_duplicate_xfbs->SetDescription(tr(TR_SKIP_DUPLICATE_XFBS_DESCRIPTION)); - m_gpu_texture_decoding->SetDescription(tr(TR_GPU_DECODING_DESCRIPTION)); m_fast_depth_calculation->SetDescription(tr(TR_FAST_DEPTH_CALC_DESCRIPTION)); m_disable_bounding_box->SetDescription(tr(TR_DISABLE_BOUNDINGBOX_DESCRIPTION)); m_save_texture_cache_state->SetDescription(tr(TR_SAVE_TEXTURE_CACHE_TO_STATE_DESCRIPTION)); @@ -261,11 +255,40 @@ void HacksWidget::AddDescriptions() m_vi_skip->SetDescription(tr(TR_VI_SKIP_DESCRIPTION)); } -void HacksWidget::UpdateGPUTextureDecodingEnabled() +void HacksWidget::UpdateGPUTextureDecodingEnabled(const QString& backend_name) { - const bool gpu_texture_decoding = g_backend_info.bSupportsGPUTextureDecoding; - m_gpu_texture_decoding->setEnabled( - gpu_texture_decoding && !Get(m_game_layer, Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION)); + static const char TR_GPU_DECODING_DESCRIPTION[] = QT_TR_NOOP( + "Enables texture decoding using the GPU instead of the CPU.

This may result in " + "performance gains in some scenarios, or on systems where the CPU is the bottleneck." + "

This setting is disabled when Arbitrary Mipmap Detection is enabled.

"); + + const bool gpu_texture_decoding_supported = g_backend_info.bSupportsGPUTextureDecoding; + const bool arbitrary_mipmap_detection_enabled = + Get(m_game_layer, Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION); + const bool gpu_texture_decoding_enabled = + gpu_texture_decoding_supported && !arbitrary_mipmap_detection_enabled; + m_gpu_texture_decoding->setEnabled(gpu_texture_decoding_enabled); + + if (!gpu_texture_decoding_supported) + { + m_gpu_texture_decoding->SetDescription(tr(TR_GPU_DECODING_DESCRIPTION) + + tr("The %1 backend doesn't support " + "GPU Texture Decoding.") + .arg(backend_name)); + } + else if (arbitrary_mipmap_detection_enabled) + { + m_gpu_texture_decoding->SetDescription( + tr(TR_GPU_DECODING_DESCRIPTION) + + tr("GPU Texture Decoding is currently disabled by Arbitrary Mipmap " + "Detection.")); + } + else + { + m_gpu_texture_decoding->SetDescription( + tr(TR_GPU_DECODING_DESCRIPTION) + + tr("If unsure, leave this unchecked.")); + } } void HacksWidget::UpdateDeferEFBCopiesEnabled() diff --git a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h index 2148c41871b0..8dd16192a72b 100644 --- a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h +++ b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h @@ -9,6 +9,7 @@ class ConfigBool; class ConfigSlider; class ConfigSliderLabel; class GraphicsPane; +class QString; namespace Config { @@ -26,7 +27,7 @@ class HacksWidget final : public QWidget void ConnectWidgets(); void AddDescriptions(); - void UpdateGPUTextureDecodingEnabled(); + void UpdateGPUTextureDecodingEnabled(const QString& backend_name); void UpdateDeferEFBCopiesEnabled(); void UpdateSkipPresentingDuplicateFramesEnabled(); From 518f5f458f16f54c5c3af4b6c49d11d9dec65db0 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 13 Apr 2026 13:34:38 -0700 Subject: [PATCH 023/270] HacksWidget: Merge BoundingBox disabled tooltip into BalloonTip When a backend doesn't support BoundingBox, show that information in the BalloonTip instead of spawning a separate tooltip. --- .../DolphinQt/Config/Graphics/HacksWidget.cpp | 38 ++++++++++++------- .../DolphinQt/Config/Graphics/HacksWidget.h | 1 + 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp index 274c789c9082..c2e04a8d4bf9 100644 --- a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp +++ b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.cpp @@ -128,15 +128,8 @@ void HacksWidget::CreateWidgets() void HacksWidget::OnBackendChanged(const QString& backend_name) { - const bool bbox = g_backend_info.bSupportsBBox; - m_disable_bounding_box->setEnabled(bbox); - - const QString tooltip = tr("%1 doesn't support this feature on your system.") - .arg(tr(backend_name.toStdString().c_str())); - - m_disable_bounding_box->setToolTip(!bbox ? tooltip : QString{}); - UpdateGPUTextureDecodingEnabled(backend_name); + UpdateBoundingBoxEnabled(backend_name); } void HacksWidget::ConnectWidgets() @@ -216,10 +209,6 @@ void HacksWidget::AddDescriptions() "Uses a less accurate algorithm to calculate depth values.

Causes issues in a few " "games, but can result in a decent speed increase depending on the game and/or " "GPU.

If unsure, leave this checked."); - static const char TR_DISABLE_BOUNDINGBOX_DESCRIPTION[] = - QT_TR_NOOP("Disables bounding box emulation.

This may improve GPU performance " - "significantly, but some games will break.

If " - "unsure, leave this checked."); static const char TR_SAVE_TEXTURE_CACHE_TO_STATE_DESCRIPTION[] = QT_TR_NOOP("Includes the contents of the embedded frame buffer (EFB) and upscaled EFB copies " "in save states. Fixes missing and/or non-upscaled textures/objects when loading " @@ -249,7 +238,6 @@ void HacksWidget::AddDescriptions() m_immediate_xfb->SetDescription(tr(TR_IMMEDIATE_XFB_DESCRIPTION)); m_skip_duplicate_xfbs->SetDescription(tr(TR_SKIP_DUPLICATE_XFBS_DESCRIPTION)); m_fast_depth_calculation->SetDescription(tr(TR_FAST_DEPTH_CALC_DESCRIPTION)); - m_disable_bounding_box->SetDescription(tr(TR_DISABLE_BOUNDINGBOX_DESCRIPTION)); m_save_texture_cache_state->SetDescription(tr(TR_SAVE_TEXTURE_CACHE_TO_STATE_DESCRIPTION)); m_vertex_rounding->SetDescription(tr(TR_VERTEX_ROUNDING_DESCRIPTION)); m_vi_skip->SetDescription(tr(TR_VI_SKIP_DESCRIPTION)); @@ -291,6 +279,30 @@ void HacksWidget::UpdateGPUTextureDecodingEnabled(const QString& backend_name) } } +void HacksWidget::UpdateBoundingBoxEnabled(const QString& backend_name) +{ + static const char TR_DISABLE_BOUNDINGBOX_DESCRIPTION[] = + QT_TR_NOOP("Disables bounding box emulation.

This may improve GPU performance " + "significantly, but some games will break.

"); + + const bool bbox = g_backend_info.bSupportsBBox; + m_disable_bounding_box->setEnabled(bbox); + + if (bbox) + { + m_disable_bounding_box->SetDescription( + tr(TR_DISABLE_BOUNDINGBOX_DESCRIPTION) + + tr("If unsure, leave this checked.")); + } + else + { + m_disable_bounding_box->SetDescription(tr(TR_DISABLE_BOUNDINGBOX_DESCRIPTION) + + tr("The %1 backend doesn't support " + "Bounding Box emulation.") + .arg(backend_name)); + } +} + void HacksWidget::UpdateDeferEFBCopiesEnabled() { // We disable the checkbox for defer EFB copies when both EFB and XFB copies to texture are diff --git a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h index 8dd16192a72b..5569e14abfc2 100644 --- a/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h +++ b/Source/Core/DolphinQt/Config/Graphics/HacksWidget.h @@ -28,6 +28,7 @@ class HacksWidget final : public QWidget void AddDescriptions(); void UpdateGPUTextureDecodingEnabled(const QString& backend_name); + void UpdateBoundingBoxEnabled(const QString& backend_name); void UpdateDeferEFBCopiesEnabled(); void UpdateSkipPresentingDuplicateFramesEnabled(); From 4a2d48f65983bcab4012e165ba0739fe324cae8e Mon Sep 17 00:00:00 2001 From: Martino Fontana Date: Sun, 19 Apr 2026 19:02:20 +0200 Subject: [PATCH 024/270] GameSettings: Add Wind Waker pre-release demo It is listed in the Dolphin wiki with the assumption that it's using the same default settings as regular Wind Waker. https://wiki.dolphin-emu.org/index.php?title=The_Legend_of_Zelda:_The_Wind_Waker_Demo_Prerelease I don't know if the Picto Box is even obtainable in the demo... but maybe it is through glitches or something, so I'll keep `EFBToTextureEnable = False` in. --- Data/Sys/GameSettings/D44.ini | 20 ++++++++++++++++++++ Data/Sys/GameSettings/GZL.ini | 14 ++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 Data/Sys/GameSettings/D44.ini diff --git a/Data/Sys/GameSettings/D44.ini b/Data/Sys/GameSettings/D44.ini new file mode 100644 index 000000000000..cd84ed5edaf3 --- /dev/null +++ b/Data/Sys/GameSettings/D44.ini @@ -0,0 +1,20 @@ +# D44J01 - The Legend of Zelda: The Wind Waker Demo Prerelease + +[Video_Hacks] +# Prevent sun flares from going through walls. +EFBAccessEnable = True +# Prevent crash when snapping a picture with the Picto Box. +EFBToTextureEnable = False + +# The game logic itself is ignoring VI interrupts +# and keeps updating shadow video registers. +# Because of this, it is incompatible with VISkip. + +VISkip = False + +[Video_Enhancements] +# Makes sea effects fade in the distance. +ArbitraryMipmapDetection = True + +[Video_Stereoscopy] +StereoConvergence = 115 diff --git a/Data/Sys/GameSettings/GZL.ini b/Data/Sys/GameSettings/GZL.ini index 4203954e7211..a8f444740430 100644 --- a/Data/Sys/GameSettings/GZL.ini +++ b/Data/Sys/GameSettings/GZL.ini @@ -1,16 +1,9 @@ -# GZLE01, GZLJ01, GZLP01 - The Legend of Zelda The Wind Waker - -[Core] -# Values set here will override the main Dolphin settings. - -[OnFrame] -# Add memory patches to be applied every frame here. - -[ActionReplay] -# Add action replay cheats here. +# GZLE01, GZLJ01, GZLP01 - The Legend of Zelda: The Wind Waker [Video_Hacks] +# Prevent sun flares from going through walls. EFBAccessEnable = True +# Prevent crash when snapping a picture with the Picto Box. EFBToTextureEnable = False # The game logic itself is ignoring VI interrupts @@ -20,6 +13,7 @@ EFBToTextureEnable = False VISkip = False [Video_Enhancements] +# Makes sea effects fade in the distance. ArbitraryMipmapDetection = True [Video_Stereoscopy] From 0d3ccfada557d912f4c307d61e4e388c871385ab Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 20 Apr 2026 12:29:29 -0700 Subject: [PATCH 025/270] ARCodeWidget: Add Enable/Disable All button --- Source/Core/DolphinQt/Config/ARCodeWidget.cpp | 47 ++++++++++++++++++- Source/Core/DolphinQt/Config/ARCodeWidget.h | 5 ++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Source/Core/DolphinQt/Config/ARCodeWidget.cpp b/Source/Core/DolphinQt/Config/ARCodeWidget.cpp index f2cc8b0e5997..e4b55c372978 100644 --- a/Source/Core/DolphinQt/Config/ARCodeWidget.cpp +++ b/Source/Core/DolphinQt/Config/ARCodeWidget.cpp @@ -4,6 +4,7 @@ #include "DolphinQt/Config/ARCodeWidget.h" #include +#include #include #include @@ -72,6 +73,7 @@ void ARCodeWidget::CreateWidgets() m_code_add = new NonDefaultQPushButton(tr("&Add New Code...")); m_code_edit = new NonDefaultQPushButton(tr("&Edit Code...")); m_code_remove = new NonDefaultQPushButton(tr("&Remove Code")); + m_code_toggle_all = new NonDefaultQPushButton(tr("Disable All")); m_cheat_code_editor = new CheatCodeEditor(this); @@ -82,6 +84,7 @@ void ARCodeWidget::CreateWidgets() button_layout->addWidget(m_code_add); button_layout->addWidget(m_code_edit); button_layout->addWidget(m_code_remove); + button_layout->addWidget(m_code_toggle_all); auto* const layout = new QVBoxLayout{this}; @@ -113,6 +116,7 @@ void ARCodeWidget::ConnectWidgets() connect(m_code_add, &QPushButton::clicked, this, &ARCodeWidget::OnCodeAddClicked); connect(m_code_edit, &QPushButton::clicked, this, &ARCodeWidget::OnCodeEditClicked); connect(m_code_remove, &QPushButton::clicked, this, &ARCodeWidget::OnCodeRemoveClicked); + connect(m_code_toggle_all, &QPushButton::clicked, this, &ARCodeWidget::OnCodeToggleAllClicked); } void ARCodeWidget::OnItemChanged(QListWidgetItem* item) @@ -122,7 +126,7 @@ void ARCodeWidget::OnItemChanged(QListWidgetItem* item) if (!m_restart_required) ActionReplay::ApplyCodes(m_ar_codes, m_game_id, m_game_revision); - UpdateList(); + UpdateToggleButton(); SaveCodes(); } @@ -157,6 +161,21 @@ void ARCodeWidget::SortDisabledCodesFirst() SaveCodes(); } +bool ARCodeWidget::IsEveryCodeEnabled() +{ + return std::ranges::all_of(m_ar_codes, &ActionReplay::ARCode::enabled); +} + +void ARCodeWidget::UpdateToggleButton() +{ + if (IsEveryCodeEnabled()) + m_code_toggle_all->setText(tr("Disable All")); + else + m_code_toggle_all->setText(tr("Enable All")); + + m_code_toggle_all->setDisabled(m_ar_codes.empty()); +} + void ARCodeWidget::OnListReordered() { // Reorder codes based on the indices of table item @@ -229,6 +248,7 @@ void ARCodeWidget::UpdateList() } m_code_list->setDragDropMode(QAbstractItemView::InternalMove); + UpdateToggleButton(); } void ARCodeWidget::LoadCodes() @@ -341,3 +361,28 @@ void ARCodeWidget::OnCodeRemoveClicked() m_code_remove->setEnabled(false); } + +void ARCodeWidget::OnCodeToggleAllClicked() +{ + const bool new_state = !IsEveryCodeEnabled(); + const Qt::CheckState new_check_state = + new_state ? Qt::CheckState::Checked : Qt::CheckState::Unchecked; + + { + // Without this blocker, the call to setCheckState below would end up loading and saving the ini + // file once per code. + QSignalBlocker blocker(m_code_list); + + for (int i = 0; i < m_ar_codes.size(); ++i) + { + m_ar_codes[i].enabled = new_state; + m_code_list->item(i)->setCheckState(new_check_state); + } + } + + if (!m_restart_required) + ActionReplay::ApplyCodes(m_ar_codes, m_game_id, m_game_revision); + + UpdateList(); + SaveCodes(); +} diff --git a/Source/Core/DolphinQt/Config/ARCodeWidget.h b/Source/Core/DolphinQt/Config/ARCodeWidget.h index 94fb8ebbe589..b3be4b905349 100644 --- a/Source/Core/DolphinQt/Config/ARCodeWidget.h +++ b/Source/Core/DolphinQt/Config/ARCodeWidget.h @@ -54,13 +54,17 @@ class ARCodeWidget : public QWidget void SortAlphabetically(); void SortEnabledCodesFirst(); void SortDisabledCodesFirst(); + void UpdateToggleButton(); void OnCodeAddClicked(); void OnCodeEditClicked(); void OnCodeRemoveClicked(); + void OnCodeToggleAllClicked(); void OnListReordered(); + bool IsEveryCodeEnabled(); + std::string m_game_id; u16 m_game_revision; @@ -72,6 +76,7 @@ class ARCodeWidget : public QWidget QPushButton* m_code_add; QPushButton* m_code_edit; QPushButton* m_code_remove; + QPushButton* m_code_toggle_all; CheatCodeEditor* m_cheat_code_editor; From 39240edc2a9f3e5791f33e9acfd7e5a3f96c6fa6 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 20 Apr 2026 13:11:00 -0700 Subject: [PATCH 026/270] GeckoCodeWidget: Add Enable/Disable All button --- .../Core/DolphinQt/Config/GeckoCodeWidget.cpp | 46 +++++++++++++++++++ .../Core/DolphinQt/Config/GeckoCodeWidget.h | 5 ++ 2 files changed, 51 insertions(+) diff --git a/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp b/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp index 74a3f5895078..3d4e799f49d0 100644 --- a/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp +++ b/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp @@ -4,6 +4,7 @@ #include "DolphinQt/Config/GeckoCodeWidget.h" #include +#include #include #include @@ -108,6 +109,7 @@ void GeckoCodeWidget::CreateWidgets() m_edit_code = new NonDefaultQPushButton(tr("&Edit Code...")); m_remove_code = new NonDefaultQPushButton(tr("&Remove Code")); m_download_codes = new NonDefaultQPushButton(tr("Download Codes")); + m_toggle_all_codes = new NonDefaultQPushButton(tr("Disable All")); m_cheat_code_editor = new CheatCodeEditor(this); @@ -145,6 +147,7 @@ void GeckoCodeWidget::CreateWidgets() btn_layout->addWidget(m_edit_code); btn_layout->addWidget(m_remove_code); btn_layout->addWidget(m_download_codes); + btn_layout->addWidget(m_toggle_all_codes); layout->addLayout(btn_layout); } @@ -163,6 +166,7 @@ void GeckoCodeWidget::ConnectWidgets() connect(m_remove_code, &QPushButton::clicked, this, &GeckoCodeWidget::RemoveCode); connect(m_edit_code, &QPushButton::clicked, this, &GeckoCodeWidget::EditCode); connect(m_download_codes, &QPushButton::clicked, this, &GeckoCodeWidget::DownloadCodes); + connect(m_toggle_all_codes, &QPushButton::clicked, this, &GeckoCodeWidget::ToggleAllCodes); connect(m_warning, &CheatWarningWidget::OpenCheatEnableSettings, this, &GeckoCodeWidget::OpenGeneralSettings); #ifdef USE_RETRO_ACHIEVEMENTS @@ -212,6 +216,7 @@ void GeckoCodeWidget::OnItemChanged(QListWidgetItem* item) if (!m_restart_required) Gecko::SetActiveCodes(m_gecko_codes, m_game_id, m_game_revision); + UpdateToggleButton(); SaveCodes(); } @@ -336,6 +341,21 @@ void GeckoCodeWidget::SortDisabledCodesFirst() SaveCodes(); } +bool GeckoCodeWidget::IsEveryCodeEnabled() +{ + return std::ranges::all_of(m_gecko_codes, &Gecko::GeckoCode::enabled); +} + +void GeckoCodeWidget::UpdateToggleButton() +{ + if (IsEveryCodeEnabled()) + m_toggle_all_codes->setText(tr("Disable All")); + else + m_toggle_all_codes->setText(tr("Enable All")); + + m_toggle_all_codes->setDisabled(m_gecko_codes.empty()); +} + void GeckoCodeWidget::OnListReordered() { // Reorder codes based on the indices of table item @@ -391,6 +411,7 @@ void GeckoCodeWidget::UpdateList() } m_code_list->setDragDropMode(QAbstractItemView::InternalMove); + UpdateToggleButton(); } void GeckoCodeWidget::DownloadCodes() @@ -438,3 +459,28 @@ void GeckoCodeWidget::DownloadCodes() tr("Downloaded %1 codes. (added %2)") .arg(QString::number(codes.size()), QString::number(added_count))); } + +void GeckoCodeWidget::ToggleAllCodes() +{ + const bool new_state = !IsEveryCodeEnabled(); + const Qt::CheckState new_check_state = + new_state ? Qt::CheckState::Checked : Qt::CheckState::Unchecked; + + { + // Without this blocker, the call to setCheckState below would end up loading and saving the ini + // file once per code. + QSignalBlocker blocker(m_code_list); + + for (int i = 0; i < m_gecko_codes.size(); ++i) + { + m_gecko_codes[i].enabled = new_state; + m_code_list->item(i)->setCheckState(new_check_state); + } + } + + if (!m_restart_required) + Gecko::SetActiveCodes(m_gecko_codes, m_game_id, m_game_revision); + + UpdateList(); + SaveCodes(); +} diff --git a/Source/Core/DolphinQt/Config/GeckoCodeWidget.h b/Source/Core/DolphinQt/Config/GeckoCodeWidget.h index a490a9f3dbfe..0ab51b8aa393 100644 --- a/Source/Core/DolphinQt/Config/GeckoCodeWidget.h +++ b/Source/Core/DolphinQt/Config/GeckoCodeWidget.h @@ -55,11 +55,15 @@ class GeckoCodeWidget : public QWidget void EditCode(); void RemoveCode(); void DownloadCodes(); + void ToggleAllCodes(); void LoadCodes(); void SaveCodes(); void SortAlphabetically(); void SortEnabledCodesFirst(); void SortDisabledCodesFirst(); + void UpdateToggleButton(); + + bool IsEveryCodeEnabled(); std::string m_game_id; std::string m_gametdb_id; @@ -78,6 +82,7 @@ class GeckoCodeWidget : public QWidget QPushButton* m_edit_code; QPushButton* m_remove_code; QPushButton* m_download_codes; + QPushButton* m_toggle_all_codes; CheatCodeEditor* m_cheat_code_editor; std::vector m_gecko_codes; bool m_restart_required; From e013d950533a5b6e6e411347f2997d4dff5f4951 Mon Sep 17 00:00:00 2001 From: naari3 Date: Sat, 25 Apr 2026 19:48:00 +0900 Subject: [PATCH 027/270] DirectoryBlob: fix data alignment for GC/Triforce and skip Triforce DIMM memory range The 0x8000 alignment in DirectoryBlob is needed for Wii disc group encryption, but for GC/Triforce it inflates file offsets unnecessarily. Use 0x20 alignment for Triforce (matching original disc layout) while keeping 0x8000 for GC due to DTK audio streaming requirements. On Triforce games with many files, the inflated offsets can land in the AMMediaboard DIMM memory range (0x1F000000-0x1F800000). Reads from that region return SRAM data instead of disc data, causing the game to hang. Skip the DIMM range when assigning per-file data offsets if any portion of the file would overlap [0x1F000000, 0x1F800000). --- Source/Core/DiscIO/DirectoryBlob.cpp | 15 ++++++++++++--- Source/Core/DiscIO/DirectoryBlob.h | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Source/Core/DiscIO/DirectoryBlob.cpp b/Source/Core/DiscIO/DirectoryBlob.cpp index fe6c3880df8e..63ff5a6eb640 100644 --- a/Source/Core/DiscIO/DirectoryBlob.cpp +++ b/Source/Core/DiscIO/DirectoryBlob.cpp @@ -895,7 +895,8 @@ DirectoryBlobPartition::DirectoryBlobPartition( const std::function* fst_nodes, FSTBuilderNode* dol_node)>& fst_callback, DirectoryBlobReader* blob) - : m_wrapped_partition(partition) + : m_wrapped_partition(partition), + m_is_triforce(volume && volume->GetVolumeType() == Platform::Triforce) { std::vector sys_nodes; @@ -1230,6 +1231,14 @@ void DirectoryBlobPartition::WriteDirectory(std::vector* fst_data, } else { + // Skip Triforce DIMM so the file doesn't overlap with it. Any portion of + // a file placed in 0x1F000000-0x1F800000 would be shadowed by SRAM on the + // AMMediaboard and read back as DIMM contents instead of disc data. + if (m_is_triforce && *data_offset < 0x1F800000 && *data_offset + entry.m_size > 0x1F000000) + { + *data_offset = 0x1F800000ull; + } + // put entry in FST WriteEntryData(fst_data, fst_offset, FILE_ENTRY, *name_offset, *data_offset, entry.m_size, m_address_shift); @@ -1243,8 +1252,8 @@ void DirectoryBlobPartition::WriteDirectory(std::vector* fst_data, std::move(content.m_source)); } - // 32 KiB aligned - many games are fine with less alignment, but not all - *data_offset = Common::AlignUp(*data_offset + entry.m_size, 0x8000ull); + const u64 data_alignment = m_is_triforce ? 0x20ull : 0x8000ull; + *data_offset = Common::AlignUp(*data_offset + entry.m_size, data_alignment); } } } diff --git a/Source/Core/DiscIO/DirectoryBlob.h b/Source/Core/DiscIO/DirectoryBlob.h index 9ee3e36fc60a..dc31aad4fc76 100644 --- a/Source/Core/DiscIO/DirectoryBlob.h +++ b/Source/Core/DiscIO/DirectoryBlob.h @@ -231,6 +231,7 @@ class DirectoryBlobPartition std::string m_root_directory; bool m_is_wii = false; + bool m_is_triforce = false; // GameCube has no shift, Wii has 2 bit shift u32 m_address_shift = 0; From 1800f5cf5177bbe8b89cf9d264e59c3a27e3cf71 Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Sun, 26 Apr 2026 12:21:06 -0400 Subject: [PATCH 028/270] Android: Convert EGLHelper to Kotlin --- .../settings/ui/SettingsFragmentPresenter.kt | 2 +- .../dolphinemu/utils/EGLHelper.java | 386 ------------------ .../dolphinemu/dolphinemu/utils/EGLHelper.kt | 176 ++++++++ 3 files changed, 177 insertions(+), 387 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 32a7a640253a..3227190e6f79 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1691,7 +1691,7 @@ class SettingsFragmentPresenter( // If we only support OpenGLES then we need both OpenGLES 3.1 and AEP val helper = EGLHelper(EGLHelper.EGL_OPENGL_ES2_BIT) - if (helper.supportsOpenGL() && helper.GetVersion() >= 320 || helper.supportsGLES3() && helper.GetVersion() >= 310 && helper.SupportsExtension( + if (helper.supportsOpenGL() && helper.getVersion() >= 320 || helper.supportsGLES3() && helper.getVersion() >= 310 && helper.supportsExtension( "GL_ANDROID_extension_pack_es31a" ) ) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java deleted file mode 100644 index b8e9dcc2cb72..000000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.utils; - -import android.opengl.GLES30; - -import org.dolphinemu.dolphinemu.NativeLibrary; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.egl.EGLSurface; -import javax.microedition.khronos.opengles.GL10; - -/** - * Utility class that abstracts all the stuff about - * EGL initialization out of the way if all that is - * wanted is to query the underlying GL API for information. - */ -public final class EGLHelper -{ - private final EGL10 mEGL; - private final EGLDisplay mDisplay; - private EGLConfig[] mEGLConfigs; - private EGLContext mEGLContext; - private EGLSurface mEGLSurface; - private GL10 mGL; - - // GL support flags - private boolean supportGL; - private boolean supportGLES2; - private boolean supportGLES3; - - // Renderable type bitmasks - public static final int EGL_OPENGL_ES_BIT = 0x0001; - public static final int EGL_OPENGL_ES2_BIT = 0x0004; - public static final int EGL_OPENGL_BIT = 0x0008; - public static final int EGL_OPENGL_ES3_BIT_KHR = 0x0040; - - // API types - public static final int EGL_OPENGL_ES_API = 0x30A0; - public static final int EGL_OPENGL_API = 0x30A2; - - /** - * Constructor - *

- * Initializes the underlying {@link EGLSurface} with a width and height of 1. - * This is useful if all you need to use this class for is to query information - * from specific API contexts. - * - * @param renderableType Bitmask indicating which types of client API contexts - * the framebuffer config must support. - */ - public EGLHelper(int renderableType) - { - this(1, 1, renderableType); - } - - /** - * Constructor - * - * @param width Width of the underlying {@link EGLSurface}. - * @param height Height of the underlying {@link EGLSurface}. - * @param renderableType Bitmask indicating which types of client API contexts - * the framebuffer config must support. - */ - public EGLHelper(int width, int height, int renderableType) - { - // Initialize handle to an EGL display. - mEGL = (EGL10) EGLContext.getEGL(); - mDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - - // If a display is present, initialize EGL. - if (mDisplay != EGL10.EGL_NO_DISPLAY) - { - int[] version = new int[2]; - if (mEGL.eglInitialize(mDisplay, version)) - { - // Detect supported GL APIs, initialize configs, etc. - detect(); - - // Create context and surface - create(width, height, renderableType); - } - else - { - Log.error("[EGLHelper] Error initializing EGL."); - } - } - else - { - Log.error("[EGLHelper] Error initializing EGL display."); - } - } - - /** - * Releases all resources associated with this helper. - *

- * This should be called whenever this helper is no longer needed. - */ - public void closeHelper() - { - mEGL.eglTerminate(mDisplay); - } - - /** - * Gets information through EGL.
- *

- * Index 0: Vendor
- * Index 1: Version
- * Index 2: Renderer
- * Index 3: Extensions
- * - * @return information retrieved through EGL. - */ - public String[] getEGLInfo() - { - return new String[]{ - mGL.glGetString(GL10.GL_VENDOR), - mGL.glGetString(GL10.GL_VERSION), - mGL.glGetString(GL10.GL_RENDERER), - mGL.glGetString(GL10.GL_EXTENSIONS), - }; - } - - /** - * Whether or not this device supports OpenGL. - * - * @return true if this device supports OpenGL; false otherwise. - */ - public boolean supportsOpenGL() - { - return supportGL; - } - - /** - * Whether or not this device supports OpenGL ES 2. - *
- * Note that if this returns true, then OpenGL ES 1 is also supported. - * - * @return true if this device supports OpenGL ES 2; false otherwise. - */ - public boolean supportsGLES2() - { - return supportGLES2; - } - - /** - * Whether or not this device supports OpenGL ES 3. - *
- * Note that if this returns true, then OpenGL ES 1 and 2 are also supported. - * - * @return true if this device supports OpenGL ES 3; false otherwise. - */ - public boolean supportsGLES3() - { - return supportGLES3; - } - - /** - * Gets the underlying {@link EGL10} instance. - * - * @return the underlying {@link EGL10} instance. - */ - public EGL10 getEGL() - { - return mEGL; - } - - /** - * Gets the underlying {@link GL10} instance. - * - * @return the underlying {@link GL10} instance. - */ - public GL10 getGL() - { - return mGL; - } - - /** - * Gets the underlying {@link EGLDisplay}. - * - * @return the underlying {@link EGLDisplay} - */ - public EGLDisplay getDisplay() - { - return mDisplay; - } - - /** - * Gets all supported framebuffer configurations for this device. - * - * @return all supported framebuffer configurations for this device. - */ - public EGLConfig[] getConfigs() - { - return mEGLConfigs; - } - - /** - * Gets the underlying {@link EGLContext}. - * - * @return the underlying {@link EGLContext}. - */ - public EGLContext getContext() - { - return mEGLContext; - } - - /** - * Gets the underlying {@link EGLSurface}. - * - * @return the underlying {@link EGLSurface}. - */ - public EGLSurface getSurface() - { - return mEGLSurface; - } - - // Detects the specific kind of GL modes that are supported - private void detect() - { - // Get total number of configs available. - int[] numConfigs = new int[1]; - if (!mEGL.eglGetConfigs(mDisplay, null, 0, numConfigs)) - { - Log.error("[EGLHelper] Error retrieving number of EGL configs available."); - return; - } - - // Now get all the configurations - mEGLConfigs = new EGLConfig[numConfigs[0]]; - if (!mEGL.eglGetConfigs(mDisplay, mEGLConfigs, mEGLConfigs.length, numConfigs)) - { - Log.error("[EGLHelper] Error retrieving all EGL configs."); - return; - } - - for (EGLConfig mEGLConfig : mEGLConfigs) - { - int[] attribVal = new int[1]; - boolean ret = - mEGL.eglGetConfigAttrib(mDisplay, mEGLConfig, EGL10.EGL_RENDERABLE_TYPE, attribVal); - if (ret) - { - if ((attribVal[0] & EGL_OPENGL_BIT) != 0) - supportGL = true; - - if ((attribVal[0] & EGL_OPENGL_ES2_BIT) != 0) - supportGLES2 = true; - - if ((attribVal[0] & EGL_OPENGL_ES3_BIT_KHR) != 0) - supportGLES3 = true; - } - } - } - - // Creates the context and surface. - private void create(int width, int height, int renderableType) - { - int[] attribs = { - EGL10.EGL_WIDTH, width, - EGL10.EGL_HEIGHT, height, - EGL10.EGL_NONE - }; - - // Initially we just assume GLES2 will be the default context. - int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - int[] ctx_attribs = { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL10.EGL_NONE - }; - - // Determine the type of context that will be created - // and change the attribute arrays accordingly. - switch (renderableType) - { - case EGL_OPENGL_ES_BIT: - ctx_attribs[1] = 1; - break; - - case EGL_OPENGL_BIT: - ctx_attribs[0] = EGL10.EGL_NONE; - break; - - case EGL_OPENGL_ES3_BIT_KHR: - ctx_attribs[1] = 3; - break; - - case EGL_OPENGL_ES2_BIT: - default: // Fall-back to GLES 2. - ctx_attribs[1] = 2; - break; - } - if (renderableType == EGL_OPENGL_BIT) - NativeLibrary.eglBindAPI(EGL_OPENGL_API); - else - NativeLibrary.eglBindAPI(EGL_OPENGL_ES_API); - - mEGLContext = - mEGL.eglCreateContext(mDisplay, mEGLConfigs[0], EGL10.EGL_NO_CONTEXT, ctx_attribs); - mEGLSurface = mEGL.eglCreatePbufferSurface(mDisplay, mEGLConfigs[0], attribs); - mEGL.eglMakeCurrent(mDisplay, mEGLSurface, mEGLSurface, mEGLContext); - mGL = (GL10) mEGLContext.getGL(); - } - - /** - * Simplified call to {@link GL10#glGetString(int)} - *

- * Accepts the following constants: - *

    - *
  • GL_VENDOR - Company responsible for the GL implementation.
  • - *
  • GL_VERSION - Version or release number.
  • - *
  • GL_RENDERER - Name of the renderer
  • - *
  • GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language
  • - *
- * - * @param glEnum A symbolic constant within {@link GL10}. - * @return the string information represented by {@code glEnum}. - */ - public String glGetString(int glEnum) - { - return mGL.glGetString(glEnum); - } - - /** - * Simplified call to {@link GLES30#glGetStringi(int, int)} - *

- * Accepts the following constants: - *

    - *
  • GL_VENDOR - Company responsible for the GL implementation.
  • - *
  • GL_VERSION - Version or release number.
  • - *
  • GL_RENDERER - Name of the renderer
  • - *
  • GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language
  • - *
  • GL_EXTENSIONS - Extension string supported by the implementation at {@code index}.
  • - *
- * - * @param glEnum A symbolic GL constant - * @param index The index of the string to return. - * @return the string information represented by {@code glEnum} and {@code index}. - */ - public String glGetStringi(int glEnum, int index) - { - return GLES30.glGetStringi(glEnum, index); - } - - public boolean SupportsExtension(String extension) - { - int[] num_ext = new int[1]; - GLES30.glGetIntegerv(GLES30.GL_NUM_EXTENSIONS, num_ext, 0); - - for (int i = 0; i < num_ext[0]; ++i) - { - String ext = GLES30.glGetStringi(GLES30.GL_EXTENSIONS, i); - if (ext.equals(extension)) - return true; - } - return false; - } - - public int GetVersion() - { - int[] major = new int[1]; - int[] minor = new int[1]; - GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, major, 0); - GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, minor, 0); - return major[0] * 100 + minor[0] * 10; - } - - /** - * Simplified call to {@link GL10#glGetIntegerv(int, int[], int) - * - * @param glEnum A symbolic GL constant. - * @return the integer information represented by {@code glEnum}. - */ - public int glGetInteger(int glEnum) - { - int[] val = new int[1]; - mGL.glGetIntegerv(glEnum, val, 0); - return val[0]; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.kt new file mode 100644 index 000000000000..df561f534f11 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2013 Dolphin Emulator Project + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +package org.dolphinemu.dolphinemu.utils + +import android.opengl.GLES30 + +import org.dolphinemu.dolphinemu.NativeLibrary + +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLDisplay +import javax.microedition.khronos.egl.EGLSurface + +/** + * Utility class that abstracts all the stuff about + * EGL initialization out of the way if all that is + * wanted is to query the underlying GL API for information. + */ +class EGLHelper(width: Int, height: Int, renderableType: Int) { + private val mEGL: EGL10 = EGLContext.getEGL() as EGL10 + private val mDisplay: EGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + private lateinit var mEGLConfigs: Array + private lateinit var mEGLContext: EGLContext + private lateinit var mEGLSurface: EGLSurface + + // GL support flags + private var supportGL = false + private var supportGLES3 = false + + /** + * Constructor + * + * Initializes the underlying [EGLSurface] with a width and height of 1. + * This is useful if all you need to use this class for is to query information + * from specific API contexts. + * + * @param renderableType Bitmask indicating which types of client API contexts + * the framebuffer config must support. + */ + constructor(renderableType: Int) : this(1, 1, renderableType) + + init { + // If a display is present, initialize EGL. + if (mDisplay != EGL10.EGL_NO_DISPLAY) { + val version = IntArray(2) + if (mEGL.eglInitialize(mDisplay, version)) { + // Detect supported GL APIs, initialize configs, etc. + detect() + + // Create context and surface + create(width, height, renderableType) + } else { + Log.error("[EGLHelper] Error initializing EGL.") + } + } else { + Log.error("[EGLHelper] Error initializing EGL display.") + } + } + + /** + * Whether or not this device supports OpenGL. + * + * @return true if this device supports OpenGL; false otherwise. + */ + fun supportsOpenGL(): Boolean { + return supportGL + } + + /** + * Whether or not this device supports OpenGL ES 3. + * + * Note that if this returns true, then OpenGL ES 1 and 2 are also supported. + * + * @return true if this device supports OpenGL ES 3; false otherwise. + */ + fun supportsGLES3(): Boolean { + return supportGLES3 + } + + // Detects the specific kind of GL modes that are supported + private fun detect() { + // Get total number of configs available. + val numConfigs = IntArray(1) + if (!mEGL.eglGetConfigs(mDisplay, null, 0, numConfigs)) { + Log.error("[EGLHelper] Error retrieving number of EGL configs available.") + return + } + + // Now get all the configurations + val eglConfigs = arrayOfNulls(numConfigs[0]) + if (!mEGL.eglGetConfigs(mDisplay, eglConfigs, eglConfigs.size, numConfigs)) { + Log.error("[EGLHelper] Error retrieving all EGL configs.") + return + } + mEGLConfigs = Array(eglConfigs.size) { i -> eglConfigs[i]!! } + + for (eglConfig in mEGLConfigs) { + val attribVal = IntArray(1) + val ret = + mEGL.eglGetConfigAttrib(mDisplay, eglConfig, EGL10.EGL_RENDERABLE_TYPE, attribVal) + if (ret) { + if ((attribVal[0] and EGL_OPENGL_BIT) != 0) supportGL = true + + if ((attribVal[0] and EGL_OPENGL_ES3_BIT_KHR) != 0) supportGLES3 = true + } + } + } + + // Creates the context and surface. + private fun create(width: Int, height: Int, renderableType: Int) { + val attribs = intArrayOf( + EGL10.EGL_WIDTH, width, EGL10.EGL_HEIGHT, height, EGL10.EGL_NONE + ) + + // Initially we just assume GLES2 will be the default context. + val eglContextClientVersion = 0x3098 + val ctxAttribs = intArrayOf( + eglContextClientVersion, 2, EGL10.EGL_NONE + ) + + // Determine the type of context that will be created + // and change the attribute arrays accordingly. + when (renderableType) { + EGL_OPENGL_ES_BIT -> ctxAttribs[1] = 1 + EGL_OPENGL_BIT -> ctxAttribs[0] = EGL10.EGL_NONE + EGL_OPENGL_ES3_BIT_KHR -> ctxAttribs[1] = 3 + EGL_OPENGL_ES2_BIT -> ctxAttribs[1] = 2 + else -> ctxAttribs[1] = 2 // Fall-back to GLES 2. + } + if (renderableType == EGL_OPENGL_BIT) { + NativeLibrary.eglBindAPI(EGL_OPENGL_API) + } else { + NativeLibrary.eglBindAPI(EGL_OPENGL_ES_API) + } + + mEGLContext = + mEGL.eglCreateContext(mDisplay, mEGLConfigs[0], EGL10.EGL_NO_CONTEXT, ctxAttribs) + mEGLSurface = mEGL.eglCreatePbufferSurface(mDisplay, mEGLConfigs[0], attribs) + mEGL.eglMakeCurrent(mDisplay, mEGLSurface, mEGLSurface, mEGLContext) + } + + fun supportsExtension(extension: String): Boolean { + val numExt = IntArray(1) + GLES30.glGetIntegerv(GLES30.GL_NUM_EXTENSIONS, numExt, 0) + + for (i in 0 until numExt[0]) { + val ext = GLES30.glGetStringi(GLES30.GL_EXTENSIONS, i) + if (ext.equals(extension)) return true + } + return false + } + + fun getVersion(): Int { + val major = IntArray(1) + val minor = IntArray(1) + GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, major, 0) + GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, minor, 0) + return major[0] * 100 + minor[0] * 10 + } + + companion object { + // Renderable type bitmasks + const val EGL_OPENGL_ES_BIT = 0x0001 + const val EGL_OPENGL_ES2_BIT = 0x0004 + const val EGL_OPENGL_BIT = 0x0008 + const val EGL_OPENGL_ES3_BIT_KHR = 0x0040 + + // API types + const val EGL_OPENGL_ES_API = 0x30A0 + const val EGL_OPENGL_API = 0x30A2 + } +} From ca3d896900b3d6850d2b80dd5daf0880fb9dbc86 Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Sun, 26 Apr 2026 12:44:24 -0400 Subject: [PATCH 029/270] Android: Convert FileBrowserHelper to Kotlin --- .../dolphinemu/utils/FileBrowserHelper.java | 175 ------------------ .../dolphinemu/utils/FileBrowserHelper.kt | 160 ++++++++++++++++ 2 files changed, 160 insertions(+), 175 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java deleted file mode 100644 index f52a70216fe1..000000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.utils; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Environment; - -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nononsenseapps.filepicker.FilePickerActivity; -import com.nononsenseapps.filepicker.Utils; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.CustomFilePickerActivity; -import org.dolphinemu.dolphinemu.features.settings.model.StringSetting; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public final class FileBrowserHelper -{ - public static final HashSet GAME_EXTENSIONS = new HashSet<>(Arrays.asList( - "gcm", "tgc", "bin", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "nfs", "wad", "dol", - "elf", "json")); - - public static final HashSet GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS); - - static - { - GAME_LIKE_EXTENSIONS.add("dff"); - } - - public static final HashSet GBA_ROM_EXTENSIONS = new HashSet<>(Arrays.asList( - "gba", "gbc", "gb", "agb", "mb", "rom", "bin")); - - public static final HashSet BIN_EXTENSION = new HashSet<>(Collections.singletonList( - "bin")); - - public static final HashSet RAW_EXTENSION = new HashSet<>(Collections.singletonList( - "raw")); - - public static final HashSet WAD_EXTENSION = new HashSet<>(Collections.singletonList( - "wad")); - - public static Intent createDirectoryPickerIntent(FragmentActivity activity, - HashSet extensions) - { - Intent i = new Intent(activity, CustomFilePickerActivity.class); - - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); - i.putExtra(FilePickerActivity.EXTRA_START_PATH, - Environment.getExternalStorageDirectory().getPath()); - i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, extensions); - - return i; - } - - @Nullable - public static String getSelectedPath(Intent result) - { - // Use the provided utility method to parse the result - List files = Utils.getSelectedFilesFromResult(result); - if (!files.isEmpty()) - { - File file = Utils.getFileForUri(files.get(0)); - return file.getAbsolutePath(); - } - - return null; - } - - public static boolean isPathEmptyOrValid(StringSetting path) - { - return isPathEmptyOrValid(path.getString()); - } - - /** - * Returns true if at least one of the following applies: - * - * 1. The input is empty. - * 2. The input is something which is not a content URI. - * 3. The input is a content URI that points to a file that exists and we're allowed to access. - */ - public static boolean isPathEmptyOrValid(String path) - { - return !ContentHandler.isContentUri(path) || ContentHandler.exists(path); - } - - public static void runAfterExtensionCheck(Context context, Uri uri, Set validExtensions, - Runnable runnable) - { - String extension = null; - - String path = uri.getLastPathSegment(); - if (path != null) - extension = getExtension(new File(path).getName(), false); - - if (extension == null) - extension = getExtension(ContentHandler.getDisplayName(uri), false); - - if (extension != null && validExtensions.contains(extension)) - { - runnable.run(); - return; - } - - String message; - if (extension == null) - { - message = context.getString(R.string.no_file_extension); - } - else - { - int messageId = validExtensions.size() == 1 ? - R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple; - - message = context.getString(messageId, extension, - setToSortedDelimitedString(validExtensions)); - } - - new MaterialAlertDialogBuilder(context) - .setMessage(message) - .setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run()) - .setNegativeButton(R.string.no, null) - .setCancelable(false) - .show(); - } - - @Nullable - public static String getExtension(@Nullable String fileName, boolean includeDot) - { - if (fileName == null) - return null; - - int dotIndex = fileName.lastIndexOf("."); - if (dotIndex == -1) - return null; - return fileName.substring(dotIndex + (includeDot ? 0 : 1)); - } - - public static String setToSortedDelimitedString(Set set) - { - ArrayList list = new ArrayList<>(set); - Collections.sort(list); - return join(", ", list); - } - - // TODO: Replace this with String.join once we can use Java 8 - private static String join(CharSequence delimiter, Iterable elements) - { - StringBuilder sb = new StringBuilder(); - - boolean first = true; - for (CharSequence element : elements) - { - if (!first) - sb.append(delimiter); - first = false; - sb.append(element); - } - - return sb.toString(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.kt new file mode 100644 index 000000000000..74f07901cbad --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.kt @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Environment +import androidx.fragment.app.FragmentActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nononsenseapps.filepicker.FilePickerActivity +import com.nononsenseapps.filepicker.Utils +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.CustomFilePickerActivity +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting +import java.io.File + +object FileBrowserHelper { + @JvmField + val GAME_EXTENSIONS: HashSet = hashSetOf( + "gcm", + "tgc", + "bin", + "iso", + "ciso", + "gcz", + "wbfs", + "wia", + "rvz", + "nfs", + "wad", + "dol", + "elf", + "json" + ) + + @JvmField + val GAME_LIKE_EXTENSIONS: HashSet = HashSet(GAME_EXTENSIONS).apply { + add("dff") + } + + @JvmField + val GBA_ROM_EXTENSIONS: HashSet = hashSetOf( + "gba", "gbc", "gb", "agb", "mb", "rom", "bin" + ) + + @JvmField + val BIN_EXTENSION: HashSet = hashSetOf("bin") + + @JvmField + val RAW_EXTENSION: HashSet = hashSetOf("raw") + + @JvmField + val WAD_EXTENSION: HashSet = hashSetOf("wad") + + @JvmStatic + fun createDirectoryPickerIntent( + activity: FragmentActivity, extensions: HashSet + ): Intent { + val intent = Intent(activity, CustomFilePickerActivity::class.java) + + intent.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + intent.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false) + intent.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) + intent.putExtra( + FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path + ) + intent.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, extensions) + + return intent + } + + @JvmStatic + fun getSelectedPath(result: Intent): String? { + // Use the provided utility method to parse the result + val files = Utils.getSelectedFilesFromResult(result) + if (files.isNotEmpty()) { + val file = Utils.getFileForUri(files[0]) + return file.absolutePath + } + + return null + } + + @JvmStatic + fun isPathEmptyOrValid(path: StringSetting): Boolean { + return isPathEmptyOrValid(path.string) + } + + /** + * Returns true if at least one of the following applies: + * + * 1. The input is empty. + * 2. The input is something which is not a content URI. + * 3. The input is a content URI that points to a file that exists and we're allowed to access. + */ + @JvmStatic + fun isPathEmptyOrValid(path: String): Boolean { + return !ContentHandler.isContentUri(path) || ContentHandler.exists(path) + } + + @JvmStatic + fun runAfterExtensionCheck( + context: Context, uri: Uri, validExtensions: Set, runnable: Runnable + ) { + var extension: String? = null + + val path = uri.lastPathSegment + if (path != null) { + extension = getExtension(File(path).name, false) + } + + if (extension == null) { + extension = getExtension(ContentHandler.getDisplayName(uri), false) + } + + if (extension != null && validExtensions.contains(extension)) { + runnable.run() + return + } + + val message = if (extension == null) { + context.getString(R.string.no_file_extension) + } else { + val messageId = if (validExtensions.size == 1) { + R.string.wrong_file_extension_single + } else { + R.string.wrong_file_extension_multiple + } + + context.getString( + messageId, extension, setToSortedDelimitedString(validExtensions) + ) + } + + MaterialAlertDialogBuilder(context).setMessage(message) + .setPositiveButton(R.string.yes) { _, _ -> runnable.run() } + .setNegativeButton(R.string.no, null).setCancelable(false).show() + } + + @JvmStatic + fun getExtension(fileName: String?, includeDot: Boolean): String? { + if (fileName == null) { + return null + } + + val dotIndex = fileName.lastIndexOf(".") + if (dotIndex == -1) { + return null + } + return fileName.substring(dotIndex + if (includeDot) 0 else 1) + } + + @JvmStatic + fun setToSortedDelimitedString(set: Set): String { + val list = ArrayList(set) + list.sort() + return list.joinToString(", ") + } +} From 3f5460a5ff318751b3effb85c4c57bbdd46e9ece Mon Sep 17 00:00:00 2001 From: Amber Brault Date: Thu, 7 May 2026 09:24:55 +0200 Subject: [PATCH 030/270] Core: Implement automatic symbol demangling --- Externals/licenses.md | 2 + Source/Core/Common/CMakeLists.txt | 2 + Source/Core/Common/CWDemangler.cpp | 772 ++++++++++++++++++ Source/Core/Common/CWDemangler.h | 67 ++ Source/Core/Common/StringUtil.cpp | 31 +- Source/Core/Common/StringUtil.h | 2 + Source/Core/Common/SymbolDB.cpp | 5 + Source/Core/Common/SymbolDB.h | 15 + Source/Core/DolphinLib.props | 2 + Source/Core/DolphinQt/Debugger/CodeWidget.cpp | 35 +- Source/Core/DolphinQt/Debugger/CodeWidget.h | 3 + Source/Core/DolphinQt/MenuBar.cpp | 8 + Source/Core/DolphinQt/MenuBar.h | 1 + Source/Core/DolphinQt/Settings.cpp | 14 + Source/Core/DolphinQt/Settings.h | 3 + Source/UnitTests/Common/CMakeLists.txt | 1 + Source/UnitTests/Common/CWDemanglerTest.cpp | 431 ++++++++++ Source/UnitTests/UnitTests.vcxproj | 1 + 18 files changed, 1388 insertions(+), 7 deletions(-) create mode 100644 Source/Core/Common/CWDemangler.cpp create mode 100644 Source/Core/Common/CWDemangler.h create mode 100644 Source/UnitTests/Common/CWDemanglerTest.cpp diff --git a/Externals/licenses.md b/Externals/licenses.md index 9db9cecf1a8d..b2d1a899576b 100644 --- a/Externals/licenses.md +++ b/Externals/licenses.md @@ -12,6 +12,8 @@ Dolphin includes or links code of the following third-party software projects: [MIT](https://github.com/mutouyun/cpp-ipc/blob/master/LICENSE) - [cubeb](https://github.com/kinetiknz/cubeb): [ISC](https://github.com/kinetiknz/cubeb/blob/master/LICENSE) +- [cwdemangle](https://github.com/encounter/cwdemangle) + [CC0-1.0](https://github.com/encounter/cwdemangle/blob/main/LICENSE) - [Discord-RPC](https://github.com/discordapp/discord-rpc): [MIT](https://github.com/discordapp/discord-rpc/blob/master/LICENSE) - [ENet](http://enet.bespin.org/): diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 894fd3812874..075975662910 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -47,6 +47,8 @@ add_library(common Crypto/HMAC.h Crypto/SHA1.cpp Crypto/SHA1.h + CWDemangler.cpp + CWDemangler.h Debug/MemoryPatches.cpp Debug/MemoryPatches.h Debug/Threads.h diff --git a/Source/Core/Common/CWDemangler.cpp b/Source/Core/Common/CWDemangler.cpp new file mode 100644 index 000000000000..8b4d700c2a15 --- /dev/null +++ b/Source/Core/Common/CWDemangler.cpp @@ -0,0 +1,772 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +// Based on: https://github.com/encounter/cwdemangle +// Copyright 2024 Luke Street +// SPDX-License-Identifier: CC0-1.0 + +#include "Common/CWDemangler.h" + +#include +#include +#include + +#include + +#include "Common/StringUtil.h" + +namespace CWDemangler +{ + +struct ParseQualifiersResult +{ + std::string pre; + std::string post; + std::string_view rest; +}; + +struct ParseDigitsResult +{ + std::size_t digits; + std::string_view rest; +}; + +inline static bool IsAscii(std::string_view s) +{ + return std::ranges::none_of(s, [](u8 c) { return c > 127; }); +} + +static const std::map operators = { + {"nw", "operator new"}, {"nwa", "operator new[]"}, + {"dl", "operator delete"}, {"dla", "operator delete[]"}, + {"pl", "operator+"}, {"mi", "operator-"}, + {"ml", "operator*"}, {"dv", "operator/"}, + {"md", "operator%"}, {"er", "operator^"}, + {"ad", "operator&"}, {"or", "operator|"}, + {"co", "operator~"}, {"nt", "operator!"}, + {"as", "operator="}, {"lt", "operator<"}, + {"gt", "operator>"}, {"apl", "operator+="}, + {"ami", "operator-="}, {"amu", "operator*="}, + {"adv", "operator/="}, {"amd", "operator%="}, + {"aer", "operator^="}, {"aad", "operator&="}, + {"aor", "operator|="}, {"ls", "operator<<"}, + {"rs", "operator>>"}, {"ars", "operator>>="}, + {"als", "operator<<="}, {"eq", "operator=="}, + {"ne", "operator!="}, {"le", "operator<="}, + {"ge", "operator>="}, {"aa", "operator&&"}, + {"oo", "operator||"}, {"pp", "operator++"}, + {"mm", "operator--"}, {"cm", "operator,"}, + {"rm", "operator->*"}, {"rf", "operator->"}, + {"cl", "operator()"}, {"vc", "operator[]"}, + {"vt", "__vtable"}}; + +static const std::map types = { + {'i', "int"}, {'b', "bool"}, {'c', "char"}, {'s', "short"}, + {'l', "long"}, {'x', "long long"}, {'f', "float"}, {'d', "double"}, + {'w', "wchar_t"}, {'v', "void"}, {'e', "..."}, {'r', "long double"}, + {'D', "short double"}, +}; + +// Finds the first double underscore in the string, excluding any that are part of a +// template argument list or operator name. +static std::optional find_split(std::string_view s, bool special, + DemangleOptions options) +{ + std::size_t start = 0; + + if (special && s.starts_with("op")) + { + const auto result = demangle_arg(s.substr(2), options); + if (!result) + return std::nullopt; + + const std::string_view rest = result->rest; + + start = s.length() - rest.length(); + } + + int depth = 0; + const std::size_t length = s.length(); + + for (std::size_t i = start; i < length; i++) + { + switch (s[i]) + { + case '<': + depth++; + break; + case '>': + depth--; + break; + case '_': + if (i < length - 1 && s[i + 1] == '_' && depth == 0) + { + return i; + } + break; + default: + break; + } + } + + return std::nullopt; +} + +ParseQualifiersResult parse_qualifiers(std::string_view str) +{ + std::string pre; + std::string post; + std::size_t index = 0; + + for (char c : str) + { + bool found_non_qualifier = false; + + switch (c) + { + case 'P': + if (pre.empty()) + { + post.insert(0, "*"); + } + else + { + post.insert(0, fmt::format("* {0}", StripTrailingWhitespace(pre))); + pre.clear(); + } + break; + case 'R': + if (pre.empty()) + { + post.insert(0, "&"); + } + else + { + post.insert(0, fmt::format("& {0}", StripTrailingWhitespace(pre))); + pre.clear(); + } + break; + case 'C': + pre += "const "; + break; + case 'V': + pre += "volatile "; + break; + case 'U': + pre += "unsigned "; + break; + case 'S': + pre += "signed "; + break; + default: + found_non_qualifier = true; + break; + } + + if (found_non_qualifier) + break; + index++; + } + str.remove_prefix(index); + post = StripTrailingWhitespace(post); + return {pre, post, str}; +} + +std::optional parse_digits(std::string_view str) +{ + if (str.empty()) + return std::nullopt; + + const char* str_start = str.data(); + const char* str_end = str_start + str.size(); + std::size_t val = 0; + std::string_view remainder; + + const auto result = std::from_chars(str_start, str_end, val); + + if (result.ec != std::errc{}) + return std::nullopt; + + const char* rest_start = result.ptr; + if (rest_start != str_end) + remainder = std::string_view(rest_start, str_end - rest_start); + + return {{val, remainder}}; +} + +std::optional demangle_template_args(std::string_view str, + DemangleOptions options) +{ + const std::size_t start_idx = str.find('<'); + + if (start_idx == std::string::npos) + return {{str, ""}}; + + const std::size_t end_idx = str.rfind('>'); + if (end_idx == std::string::npos || end_idx < start_idx) + { + return std::nullopt; + } + + std::string_view args(&str[start_idx + 1], &str[end_idx]); + const std::string_view template_name = str.substr(0, start_idx); + std::string tmpl_args = "<"; + + while (!args.empty()) + { + const auto result = demangle_arg(args, options); + if (!result) + return std::nullopt; + + const auto demangled_arg = result.value(); + const std::string_view rest = demangled_arg.rest; + tmpl_args += demangled_arg.arg_pre; + tmpl_args += demangled_arg.arg_post; + + if (rest.empty()) + break; + + tmpl_args += ", "; + + args = rest.substr(1); + } + + tmpl_args += ">"; + + return {{template_name, tmpl_args}}; +} + +std::optional demangle_name(std::string_view str, DemangleOptions options) +{ + const auto result = parse_digits(str); + if (!result) + return std::nullopt; + + auto [size, rest] = result.value(); + + if (rest.length() < size) + { + return std::nullopt; + } + + auto result1 = demangle_template_args(rest.substr(0, size), options); + if (!result1) + return std::nullopt; + + auto [name, args] = result1.value(); + const std::string result_name{name}; + return {{result_name, fmt::format("{0}{1}", name, args), rest.substr(size)}}; +} + +std::optional demangle_qualified_name(std::string_view str, + DemangleOptions options) +{ + if (!str.starts_with('Q')) + return demangle_name(str, options); + + if (str.length() < 3) + { + return std::nullopt; + } + + // MWCC only preserves up to 9 layers of namespace/class depth in symbol names, so we + // can just check one digit + const int digit = static_cast(str[1] - '0'); + if (digit < 0 || digit > 9) + return std::nullopt; + + const int count = digit; + + str = str.substr(2); + + std::string last_class; + std::string qualified; + + for (int i = 0; i < count; i++) + { + const auto result = demangle_name(str, options); + if (!result) + return std::nullopt; + + auto [class_name, full, rest] = *result; + qualified += full; + last_class = class_name; + str = rest; + if (i < count - 1) + { + qualified += "::"; + } + } + return {{last_class, qualified, str}}; +} + +std::optional demangle_arg(std::string_view str, DemangleOptions options) +{ + // Negative constant + if (str.starts_with('-')) + { + const auto parse_result = parse_digits(str.substr(1)); + if (!parse_result) + return std::nullopt; + + const std::size_t size = parse_result->digits; + const std::string out_val = fmt::format("-{}", size); + return {{out_val, "", parse_result->rest}}; + } + + std::string result; + + const auto parse_qual_result = parse_qualifiers(str); + std::string pre = parse_qual_result.pre; + std::string post = parse_qual_result.post; + std::string_view rest = parse_qual_result.rest; + result += pre; + str = rest; + + // Disambiguate arguments starting with a number + if (str.length() > 0 && std::isdigit(static_cast(str[0]))) + { + const auto parse_result = parse_digits(str); + if (!parse_result) + return std::nullopt; + + auto& [num, rest_value] = parse_result.value(); + rest = rest_value; + + // If the number is followed by a comma or the end of the string, it's a template argument + if (rest.empty() || rest.starts_with(',')) + { + // ...or a Metrowerks extension type + if (options.mw_extensions) + { + const std::string t = num == 1 ? "__int128" : num == 2 ? "__vec2x32float__" : ""; + + if (!t.empty()) + { + result += t; + return {{result, post, rest}}; + } + } + result += fmt::format("{}{}", num, post); + return {{result, "", rest}}; + } + // Otherwise, it's (probably) the size of a type + const auto demangle_name_result = demangle_name(str, options); + if (!demangle_name_result) + return std::nullopt; + + result += demangle_name_result->full; + result += post; + return {{result, "", demangle_name_result->rest}}; + } + + // Handle qualified names + if (str.starts_with('Q')) + { + const auto demangle_qual_result = demangle_qualified_name(str, options); + if (!demangle_qual_result) + return std::nullopt; + + result += demangle_qual_result->full; + result += post; + return {{result, "", demangle_qual_result->rest}}; + } + + bool is_member = false; + bool const_member = false; + if (str.starts_with('M')) + { + is_member = true; + const auto demangle_qual_result = demangle_qualified_name(str.substr(1), options); + if (!demangle_qual_result) + return std::nullopt; + + rest = demangle_qual_result->rest; + pre = fmt::format("{}::*{}", demangle_qual_result->full, pre); + if (!rest.starts_with('F')) + { + return std::nullopt; + } + str = rest; + } + if (is_member || str.starts_with('F')) + { + str.remove_prefix(1); + if (is_member) + { + // "const void*, const void*" or "const void*, void*" + if (str.starts_with("PCvPCv")) + { + const_member = true; + str.remove_prefix(6); + } + else if (str.starts_with("PCvPv")) + { + str.remove_prefix(5); + } + else + { + return std::nullopt; + } + } + else if (post.starts_with('*')) + { + post = StripLeadingWhitespace(post.substr(1)); + pre = fmt::format("*{}", pre); + } + else + { + return std::nullopt; + } + + const auto demangle_func_args_result = demangle_function_args(str, options); + if (!demangle_func_args_result) + return std::nullopt; + + const auto demangled_func_args = demangle_func_args_result.value(); + + if (!demangled_func_args.rest.starts_with('_')) + { + return std::nullopt; + } + + const auto demangle_arg_result = demangle_arg(demangled_func_args.rest.substr(1), options); + if (!demangle_arg_result) + return std::nullopt; + + const std::string_view const_str = const_member ? " const" : ""; + const std::string res_pre = fmt::format("{} ({}{}", demangle_arg_result->arg_pre, pre, post); + const std::string res_post = fmt::format(")({}){}{}", demangled_func_args.args, const_str, + demangle_arg_result->arg_post); + return {{res_pre, res_post, demangle_arg_result->rest}}; + } + + if (str.starts_with('A')) + { + const auto parse_result = parse_digits(str.substr(1)); + if (!parse_result) + return std::nullopt; + + auto& [count, rest_value] = parse_result.value(); + rest = rest_value; + + if (!rest.starts_with('_')) + { + return std::nullopt; + } + + const auto demangle_arg_result = demangle_arg(rest.substr(1), options); + if (!demangle_arg_result) + return std::nullopt; + + if (!post.empty()) + { + post = fmt::format("({})", post); + } + result = fmt::format("{}{}{}", pre, demangle_arg_result->arg_pre, post); + const std::string ret_post = fmt::format("[{}]{}", count, demangle_arg_result->arg_post); + return {{result, ret_post, demangle_arg_result->rest}}; + } + + if (str.length() == 0) + return std::nullopt; + + std::string_view type; + + const char c = str[0]; + + if (types.contains(c)) + { + type = types.at(c); + } + else + { + // Handle special cases + switch (c) + { + case '1': + if (options.mw_extensions) + type = "__int128"; + break; + case '2': + if (options.mw_extensions) + type = "__vec2x32float__"; + break; + case '_': + return {{result, "", rest}}; + default: + return std::nullopt; + } + } + + return {{fmt::format("{}{}{}", result, type, post), "", str.substr(1)}}; +} + +std::optional demangle_function_args(std::string_view str, + DemangleOptions options) +{ + std::string result; + + while (!str.empty()) + { + if (!result.empty()) + { + result += ", "; + } + + const auto demangle_arg_result = demangle_arg(str, options); + if (!demangle_arg_result) + return std::nullopt; + + result += demangle_arg_result->arg_pre; + result += demangle_arg_result->arg_post; + str = demangle_arg_result->rest; + + if (str.starts_with('_') || str.starts_with(',')) + { + break; + } + } + + return {{result, str}}; +} + +std::optional demangle_special_function(std::string_view str, + std::string_view class_name, + DemangleOptions options) +{ + if (str.starts_with("op")) + { + const std::string_view rest = str.substr(2); + const auto demangle_arg_result = demangle_arg(rest, options); + if (!demangle_arg_result) + return std::nullopt; + + return fmt::format("operator {}{}", demangle_arg_result->arg_pre, + demangle_arg_result->arg_post); + } + + const auto result = demangle_template_args(str, options); + if (!result) + return std::nullopt; + + auto& [op, args] = result.value(); + + std::string_view func_name; + + if (op == "dt") + { + return fmt::format("~{}{}", class_name, args); + } + else if (op == "ct") + { + func_name = class_name; + } + else if (operators.contains(op.data())) + { + func_name = operators.at(op.data()); + } + else + { + return fmt::format("__{}{}", op, args); + } + + return fmt::format("{0}{1}", func_name, args); +} + +// Demangle a symbol name. +// +// Returns `std::nullopt` if the input is not a valid mangled name. +std::optional demangle(std::string_view str, DemangleOptions options) +{ + if (!IsAscii(str)) + { + return std::nullopt; + } + + bool special = false; + bool cnst = false; + std::string fn_name; + std::string static_var; + + // Handle new static function variables (Wii CW) + const bool guard = str.starts_with("@GUARD@"); + if (guard || str.starts_with("@LOCAL@")) + { + str = str.substr(7); + std::size_t idx = str.rfind('@'); + if (idx == std::string::npos) + return std::nullopt; + + const std::string_view rest = str.substr(0, idx); + const std::string_view var = str.substr(idx); + + if (guard) + { + static_var = fmt::format("{0} guard", var.substr(1)); + } + else + { + static_var = var.substr(1); + } + str = rest; + } + + if (str.starts_with("__")) + { + special = true; + str = str.substr(2); + } + + const auto idx_temp = find_split(str, special, options); + if (!idx_temp) + return std::nullopt; + std::size_t idx = idx_temp.value(); + // Handle any trailing underscores in the function name + while (str[idx + 2] == '_') + { + idx++; + } + + const std::string_view fn_name_out = str.substr(0, idx); + std::string_view rest = str.substr(idx); + + if (special) + { + if (fn_name_out == "init") + { + // Special case for double __ + const std::size_t rest_idx = rest.substr(2).find("__"); + if (rest_idx == std::string::npos) + return std::nullopt; + fn_name = str.substr(0, rest_idx + 6); + rest.remove_prefix(rest_idx + 2); + } + else + { + fn_name = fn_name_out; + } + } + else + { + const auto result = demangle_template_args(fn_name_out, options); + if (!result) + return std::nullopt; + + const auto template_args = result.value(); + fn_name = fmt::format("{}{}", template_args.name, template_args.args); + } + + // Handle old static function variables (GC CW) + const std::size_t first_idx = fn_name.find('$'); + if (first_idx != std::string::npos) + { + const std::size_t second_idx = fn_name.substr(first_idx + 1).find('$'); + if (second_idx == std::string::npos) + return std::nullopt; + + const std::string var = fn_name.substr(0, first_idx); + std::string rest_temp = fn_name.substr(first_idx + 1); + + const std::string var_type = rest_temp.substr(0, second_idx); + rest_temp = rest_temp.substr(second_idx); + + if (!var_type.starts_with("localstatic")) + { + return std::nullopt; + } + + if (var == "init") + { + // Sadly, $localstatic doesn't provide the variable name in guard/init + static_var = fmt::format("{} guard", var_type); + } + else + { + static_var = var; + } + + fn_name = rest_temp.substr(1); + } + + str = rest.substr(2); + + std::string class_name; + std::string return_type_pre; + std::string return_type_post; + std::string qualified; + + if (!str.starts_with('F')) + { + const auto result = demangle_qualified_name(str, options); + if (!result) + return std::nullopt; + + class_name = result->class_name; + qualified = result->full; + str = result->rest; + } + if (special) + { + const auto result = demangle_special_function(fn_name, class_name, options); + if (!result) + return std::nullopt; + fn_name = result.value(); + } + if (str.starts_with('C')) + { + str.remove_prefix(1); + cnst = true; + } + if (str.starts_with('F')) + { + str.remove_prefix(1); + const auto result = demangle_function_args(str, options); + if (!result) + return std::nullopt; + + if (options.omit_empty_parameters && result->args == "void") + { + fn_name = fmt::format("{}()", fn_name); + } + else + { + fn_name = fmt::format("{}({})", fn_name, result->args); + } + str = result->rest; + } + if (str.starts_with('_')) + { + str.remove_prefix(1); + const auto result = demangle_arg(str, options); + if (!result) + return std::nullopt; + + return_type_pre = result->arg_pre; + return_type_post = result->arg_post; + str = result->rest; + } + if (!str.empty()) + { + return std::nullopt; + } + if (cnst) + { + fn_name = fmt::format("{} const", fn_name); + } + if (!qualified.empty()) + { + fn_name = fmt::format("{}::{}", qualified, fn_name); + } + if (!return_type_pre.empty()) + { + fn_name = fmt::format("{} {}{}", return_type_pre, fn_name, return_type_post); + } + if (!static_var.empty()) + { + fn_name = fmt::format("{}::{}", fn_name, static_var); + } + + return fn_name; +} + +} // namespace CWDemangler diff --git a/Source/Core/Common/CWDemangler.h b/Source/Core/Common/CWDemangler.h new file mode 100644 index 000000000000..b15aa9492659 --- /dev/null +++ b/Source/Core/Common/CWDemangler.h @@ -0,0 +1,67 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +// Based on: https://github.com/encounter/cwdemangle +// Copyright 2024 Luke Street +// SPDX-License-Identifier: CC0-1.0 + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +namespace CWDemangler +{ + +// Options for [demangle]. +struct DemangleOptions +{ + // Replace `(void)` function parameters with `()` + bool omit_empty_parameters = true; + // Enable Metrowerks extension types (`__int128`, `__vec2x32float__`, etc.) + // + // Disabled by default since they conflict with template argument literals + // and can't always be demangled correctly. + bool mw_extensions = false; +}; + +struct DemangleTemplateArgsResult +{ + std::string_view name; + std::string args; +}; + +struct DemangleNameResult +{ + std::string class_name; + std::string full; + std::string_view rest; +}; + +struct DemangleArgResult +{ + std::string arg_pre; + std::string arg_post; + std::string_view rest; +}; + +struct DemangleFunctionArgsResult +{ + std::string args; + std::string_view rest; +}; + +std::optional demangle_template_args(std::string_view str, + DemangleOptions options); +std::optional demangle_name(std::string_view str, DemangleOptions options); +std::optional demangle_qualified_name(std::string_view str, + DemangleOptions options); +std::optional demangle_arg(std::string_view str, DemangleOptions options); +std::optional demangle_function_args(std::string_view str, + DemangleOptions options); +std::optional demangle_special_function(std::string_view str, + std::string_view class_name, + DemangleOptions options); +std::optional demangle(std::string_view str, DemangleOptions options); +} // namespace CWDemangler diff --git a/Source/Core/Common/StringUtil.cpp b/Source/Core/Common/StringUtil.cpp index ed4837b1122b..e84fabcbb583 100644 --- a/Source/Core/Common/StringUtil.cpp +++ b/Source/Core/Common/StringUtil.cpp @@ -193,7 +193,7 @@ std::string ArrayToString(const u8* data, u32 size, int line_len, bool spaces) template static std::string_view StripEnclosingChars(std::string_view str, T chars) { - const size_t s = str.find_first_not_of(chars); + const std::size_t s = str.find_first_not_of(chars); if (str.npos != s) return str.substr(s, str.find_last_not_of(chars) - s + 1); @@ -201,12 +201,41 @@ static std::string_view StripEnclosingChars(std::string_view str, T chars) return ""; } +template +static std::string_view StripLeadingChars(std::string_view str, T chars) +{ + const std::size_t s = str.find_first_not_of(chars); + + if (str.npos != s) + return str.substr(s); + else + return ""; +} + +template +static std::string_view StripTrailingChars(std::string_view str, T chars) +{ + return str.substr(0, str.find_last_not_of(chars) + 1); +} + // Turns "\n\r\t hello " into "hello" (trims at the start and end but not inside). std::string_view StripWhitespace(std::string_view str) { return StripEnclosingChars(str, " \t\r\n"); } +// Turns "\n\r\t hello " into "hello " (trims at the start). +std::string_view StripLeadingWhitespace(std::string_view str) +{ + return StripLeadingChars(str, " \t\r\n"); +} + +// Turns "\n\r\t hello " into "\n\r\t hello" (trims at the end). +std::string_view StripTrailingWhitespace(std::string_view str) +{ + return StripTrailingChars(str, " \t\r\n"); +} + std::string_view StripSpaces(std::string_view str) { return StripEnclosingChars(str, ' '); diff --git a/Source/Core/Common/StringUtil.h b/Source/Core/Common/StringUtil.h index 0cc4e6c15e34..61404376d385 100644 --- a/Source/Core/Common/StringUtil.h +++ b/Source/Core/Common/StringUtil.h @@ -49,6 +49,8 @@ inline void CharArrayFromFormat(char (&out)[Count], const char* format, ...) std::string ArrayToString(const u8* data, u32 size, int line_len = 20, bool spaces = true); std::string_view StripWhitespace(std::string_view s); +std::string_view StripLeadingWhitespace(std::string_view s); +std::string_view StripTrailingWhitespace(std::string_view s); std::string_view StripSpaces(std::string_view s); std::string_view StripQuotes(std::string_view s); diff --git a/Source/Core/Common/SymbolDB.cpp b/Source/Core/Common/SymbolDB.cpp index 2fa514ba865d..e1778d2ca5a5 100644 --- a/Source/Core/Common/SymbolDB.cpp +++ b/Source/Core/Common/SymbolDB.cpp @@ -9,6 +9,7 @@ #include #include +#include "Common/CWDemangler.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" @@ -31,6 +32,10 @@ void Symbol::Rename(const std::string& symbol_name) { this->name = symbol_name; this->function_name = GetStrippedFunctionName(symbol_name); + + // Try demangling the symbol name, saving it in the symbol if valid. + auto demangle_result = CWDemangler::demangle(symbol_name, CWDemangler::DemangleOptions()); + this->demangled_name = demangle_result.value_or(""); } void SymbolDB::List() diff --git a/Source/Core/Common/SymbolDB.h b/Source/Core/Common/SymbolDB.h index 8a1767d696b5..5074b03ad577 100644 --- a/Source/Core/Common/SymbolDB.h +++ b/Source/Core/Common/SymbolDB.h @@ -54,7 +54,22 @@ struct Symbol void Rename(const std::string& symbol_name); + bool IsDemangled() const { return !demangled_name.empty(); } + + const std::string& GetDisplayName(bool use_demangled_name) const + { + if (use_demangled_name && IsDemangled()) + { + return demangled_name; + } + else + { + return name; + } + } + std::string name; + std::string demangled_name; // Demangled symbol name std::string function_name; // stripped function name std::string object_name; // name of object/source file symbol belongs to std::vector callers; // addresses of functions that call this function diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 9959caf60cf8..5ecd90d543ab 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -48,6 +48,7 @@ + @@ -847,6 +848,7 @@ + diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp index a04f761cee33..edac7e52b931 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp @@ -73,6 +73,9 @@ CodeWidget::CodeWidget(QWidget* parent) connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &CodeWidget::Update); + connect(&Settings::Instance(), &Settings::ShowDemangledNamesChanged, this, + &CodeWidget::OnShowDemangledNamesChanged); + ConnectWidgets(); m_code_splitter->restoreState( @@ -248,6 +251,16 @@ void CodeWidget::OnSetCodeAddress(u32 address) SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); } +void CodeWidget::OnShowDemangledNamesChanged() +{ + UpdateSymbols(); + if (const Common::Symbol* symbol = m_ppc_symbol_db.GetSymbolFromAddr(m_code_view->GetAddress())) + { + UpdateFunctionCalls(symbol); + UpdateFunctionCallers(symbol); + } +} + void CodeWidget::OnPPCSymbolsChanged() { UpdateSymbols(); @@ -424,7 +437,7 @@ void CodeWidget::UpdateSymbols() m_symbols_list->clear(); m_ppc_symbol_db.ForEachSymbol([&](const Common::Symbol& symbol) { - QString name = QString::fromStdString(symbol.name); + QString name = QString::fromStdString(GetSymbolDisplayName(&symbol)); // If the symbol has an object name, add it to the entry name. if (!symbol.object_name.empty()) @@ -488,15 +501,16 @@ void CodeWidget::UpdateFunctionCalls(const Common::Symbol* symbol) if (call_symbol) { QString name; + const std::string& symbol_name = GetSymbolDisplayName(call_symbol); if (!call_symbol->object_name.empty()) { name = QString::fromStdString( - fmt::format("< {} ({}, {:08x})", call_symbol->name, call_symbol->object_name, addr)); + fmt::format("< {} ({}, {:08x})", symbol_name, call_symbol->object_name, addr)); } else { - name = QString::fromStdString(fmt::format("< {} ({:08x})", call_symbol->name, addr)); + name = QString::fromStdString(fmt::format("< {} ({:08x})", symbol_name, addr)); } if (!name.contains(filter, Qt::CaseInsensitive)) @@ -525,15 +539,16 @@ void CodeWidget::UpdateFunctionCallers(const Common::Symbol* symbol) if (caller_symbol) { QString name; + const std::string& symbol_name = GetSymbolDisplayName(caller_symbol); if (!caller_symbol->object_name.empty()) { - name = QString::fromStdString(fmt::format("< {} ({}, {:08x})", caller_symbol->name, - caller_symbol->object_name, addr)); + name = QString::fromStdString( + fmt::format("< {} ({}, {:08x})", symbol_name, caller_symbol->object_name, addr)); } else { - name = QString::fromStdString(fmt::format("< {} ({:08x})", caller_symbol->name, addr)); + name = QString::fromStdString(fmt::format("< {} ({:08x})", symbol_name, addr)); } if (!name.contains(filter, Qt::CaseInsensitive)) @@ -546,6 +561,14 @@ void CodeWidget::UpdateFunctionCallers(const Common::Symbol* symbol) } } +// Gets the name of this symbol based on the option for whether or not to show +// demangled names. +const std::string& CodeWidget::GetSymbolDisplayName(const Common::Symbol* symbol) const +{ + const bool show_demangled_names = Settings::Instance().IsShowDemangledNames(); + return symbol->GetDisplayName(show_demangled_names); +} + void CodeWidget::Step() { auto& cpu = m_system.GetCPU(); diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.h b/Source/Core/DolphinQt/Debugger/CodeWidget.h index 76ada4f5068f..9a678e1c2c1a 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.h +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.h @@ -64,6 +64,7 @@ class CodeWidget : public QDockWidget void UpdateFunctionCallers(const Common::Symbol* symbol); void UpdateNotes(); + void OnShowDemangledNamesChanged(); void OnPPCSymbolsChanged(); void OnSearchAddress(); void OnSearchSymbols(); @@ -76,6 +77,8 @@ class CodeWidget : public QDockWidget void closeEvent(QCloseEvent*) override; void showEvent(QShowEvent* event) override; + const std::string& GetSymbolDisplayName(const Common::Symbol* symbol) const; + Core::System& m_system; PPCSymbolDB& m_ppc_symbol_db; diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index f5d87e0b775f..a29ba272ecb2 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -1073,6 +1073,7 @@ void MenuBar::AddSymbolsMenu() generate->addAction(tr("Address"), this, &MenuBar::GenerateSymbolsFromAddress); generate->addAction(tr("Signature Database"), this, &MenuBar::GenerateSymbolsFromSignatureDB); generate->addAction(tr("RSO Modules"), this, &MenuBar::GenerateSymbolsFromRSO); + m_debugger_show_demangled_names = m_symbols->addAction(tr("Show &Demangled Names")); m_symbols->addSeparator(); m_symbols->addAction(tr("&Load Symbol Map"), this, &MenuBar::LoadSymbolMap); @@ -1096,6 +1097,13 @@ void MenuBar::AddSymbolsMenu() m_symbols->addSeparator(); m_symbols->addAction(tr("&Patch HLE Functions"), this, &MenuBar::PatchHLEFunctions); + + m_debugger_show_demangled_names->setCheckable(true); + m_debugger_show_demangled_names->setChecked(Settings::Instance().IsShowDemangledNames()); + connect(m_debugger_show_demangled_names, &QAction::toggled, &Settings::Instance(), + &Settings::SetShowDemangledNames); + connect(&Settings::Instance(), &Settings::ShowDemangledNamesChanged, + m_debugger_show_demangled_names, &QAction::setChecked); } void MenuBar::UpdateToolsMenu(const Core::State state) diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index a381353b0ce6..153592451067 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -310,6 +310,7 @@ class MenuBar final : public QMenuBar QAction* m_jit_systemregisters_off; QAction* m_jit_branch_off; QAction* m_jit_register_cache_off; + QAction* m_debugger_show_demangled_names; bool m_game_selected = false; diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp index 595e9d808496..618a2b5ced82 100644 --- a/Source/Core/DolphinQt/Settings.cpp +++ b/Source/Core/DolphinQt/Settings.cpp @@ -871,6 +871,20 @@ QFont Settings::GetDebugFont() const return GetQSettings().value(QStringLiteral("debugger/font"), default_font).value(); } +void Settings::SetShowDemangledNames(bool enabled) +{ + if (IsShowDemangledNames() == enabled) + return; + QSettings().setValue(QStringLiteral("debugger/showdemanglednames"), enabled); + + emit ShowDemangledNamesChanged(enabled); +} + +bool Settings::IsShowDemangledNames() const +{ + return QSettings().value(QStringLiteral("debugger/showdemanglednames")).toBool(); +} + void Settings::SetAutoUpdateTrack(const QString& mode) { if (mode == GetAutoUpdateTrack()) diff --git a/Source/Core/DolphinQt/Settings.h b/Source/Core/DolphinQt/Settings.h index 2fe0d272e63a..0c79020ff8a2 100644 --- a/Source/Core/DolphinQt/Settings.h +++ b/Source/Core/DolphinQt/Settings.h @@ -176,6 +176,8 @@ class Settings final : public QObject bool IsAssemblerVisible() const; QFont GetDebugFont() const; void SetDebugFont(const QFont& font); + void SetShowDemangledNames(bool enabled); + bool IsShowDemangledNames() const; // Auto-Update QString GetAutoUpdateTrack() const; @@ -223,6 +225,7 @@ class Settings final : public QObject void AssemblerVisibilityChanged(bool visible); void DebugModeToggled(bool enabled); void DebugFontChanged(const QFont& font); + void ShowDemangledNamesChanged(bool enabled); void AutoUpdateTrackChanged(const QString& mode); void FallbackRegionChanged(const DiscIO::Region& region); void AnalyticsToggled(bool enabled); diff --git a/Source/UnitTests/Common/CMakeLists.txt b/Source/UnitTests/Common/CMakeLists.txt index a2e21d7a8024..f7689b3ec4eb 100644 --- a/Source/UnitTests/Common/CMakeLists.txt +++ b/Source/UnitTests/Common/CMakeLists.txt @@ -7,6 +7,7 @@ add_dolphin_test(BusyLoopTest BusyLoopTest.cpp) add_dolphin_test(CommonFuncsTest CommonFuncsTest.cpp) add_dolphin_test(CryptoEcTest Crypto/EcTest.cpp) add_dolphin_test(CryptoSHA1Test Crypto/SHA1Test.cpp) +add_dolphin_test(CWDemanglerTest CWDemanglerTest.cpp) add_dolphin_test(EnumFormatterTest EnumFormatterTest.cpp) add_dolphin_test(EventTest EventTest.cpp) add_dolphin_test(FileUtilTest FileUtilTest.cpp) diff --git a/Source/UnitTests/Common/CWDemanglerTest.cpp b/Source/UnitTests/Common/CWDemanglerTest.cpp new file mode 100644 index 000000000000..7320e48472a7 --- /dev/null +++ b/Source/UnitTests/Common/CWDemanglerTest.cpp @@ -0,0 +1,431 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +// Based on: https://github.com/encounter/cwdemangle +// Copyright 2024 Luke Street +// SPDX-License-Identifier: CC0-1.0 + +#include +#include +#include +#include + +#include "Common/CWDemangler.h" + +using namespace CWDemangler; + +void DoDemangleTemplateArgsTest(std::string mangled, std::string name, std::string template_args) +{ + DemangleOptions options = DemangleOptions(); + + auto result = demangle_template_args(mangled, options); + DemangleTemplateArgsResult expected = {name, template_args}; + + EXPECT_TRUE(result.has_value()); + if (result.has_value()) + { + EXPECT_EQ(result.value().args, expected.args); + EXPECT_EQ(result.value().name, expected.name); + } +} + +void DoDemangleNameTest(std::string mangled, std::string name, std::string full_name) +{ + DemangleOptions options = DemangleOptions(); + + auto result = demangle_name(mangled, options); + DemangleNameResult expected = {name, full_name, ""}; + + EXPECT_TRUE(result.has_value()); + if (result.has_value()) + { + EXPECT_EQ(result.value().class_name, expected.class_name); + EXPECT_EQ(result.value().full, expected.full); + EXPECT_EQ(result.value().rest, expected.rest); + } +} + +void DoDemangleQualifiedNameTest(std::string mangled, std::string base_name, std::string full_name) +{ + DemangleOptions options = DemangleOptions(); + + auto result = demangle_qualified_name(mangled, options); + DemangleNameResult expected = {base_name, full_name, ""}; + + EXPECT_TRUE(result.has_value()); + if (result.has_value()) + { + EXPECT_EQ(result.value().class_name, expected.class_name); + EXPECT_EQ(result.value().full, expected.full); + EXPECT_EQ(result.value().rest, expected.rest); + } +} + +void DoDemangleArgTest(std::string mangled, std::string type_pre, std::string type_post, + std::string remainder) +{ + DemangleOptions options = DemangleOptions(); + + auto result = CWDemangler::demangle_arg(mangled, options); + DemangleArgResult expected = {type_pre, type_post, remainder}; + + EXPECT_TRUE(result.has_value()); + if (result.has_value()) + { + EXPECT_EQ(result.value().arg_post, expected.arg_post); + EXPECT_EQ(result.value().arg_pre, expected.arg_pre); + EXPECT_EQ(result.value().rest, expected.rest); + } +} + +void DoDemangleFunctionArgsTest(std::string mangled, std::string args, std::string remainder) +{ + DemangleOptions options = DemangleOptions(); + + auto result = demangle_function_args(mangled, options); + DemangleFunctionArgsResult expected = {args, remainder}; + + EXPECT_TRUE(result.has_value()); + if (result.has_value()) + { + EXPECT_EQ(result.value().args, expected.args); + EXPECT_EQ(result.value().rest, expected.rest); + } +} + +void DoDemangleTest(std::string mangled, std::string demangled) +{ + DemangleOptions options = DemangleOptions(); + + auto result = demangle(mangled, options); + std::optional expected = {demangled}; + if (demangled == "") + expected = std::nullopt; + + EXPECT_EQ(result, expected); +} + +void DoDemangleOptionsTest(bool omit_empty_params, bool mw_extensions, std::string mangled, + std::string demangled) +{ + DemangleOptions options = DemangleOptions(omit_empty_params, mw_extensions); + + auto result = demangle(mangled, options); + std::optional expected = {demangled}; + + EXPECT_EQ(result, expected); +} + +TEST(CWDemangler, TestDemangleTemplateArgs) +{ + DoDemangleTemplateArgsTest("ShaderUid<24geometry_shader_uid_data>", "ShaderUid", + ""); + DoDemangleTemplateArgsTest("basic_string,Q23std12allocator>", + "basic_string", + ", std::allocator>"); +} + +TEST(CWDemangler, TestDemangleName) +{ + DoDemangleNameTest("37ShaderUid<24geometry_shader_uid_data>", "ShaderUid", + "ShaderUid"); + DoDemangleNameTest("59basic_string,Q23std12allocator>", + "basic_string", + "basic_string, std::allocator>"); +} + +TEST(CWDemangler, TestDemangleQualifiedName) +{ + DoDemangleQualifiedNameTest("7Wiimote", "Wiimote", "Wiimote"); + DoDemangleQualifiedNameTest("Q23Gen8XEmitter", "XEmitter", "Gen::XEmitter"); + DoDemangleQualifiedNameTest( + "Q23std59basic_string,Q23std12allocator>", "basic_string", + "std::basic_string, std::allocator>"); +} + +TEST(CWDemangler, TestDemangleArg) +{ + DoDemangleArgTest("v", "void", "", ""); + DoDemangleArgTest("b", "bool", "", ""); + DoDemangleArgTest("RC9CVector3fUc", "const CVector3f&", "", "Uc"); + DoDemangleArgTest("Q23std14char_traits,", "std::char_traits", "", ","); + DoDemangleArgTest("PFPCcPCc_v", "void (*", ")(const char*, const char*)", ""); + DoDemangleArgTest("RCPCVPCVUi", "const volatile unsigned int* const volatile* const&", "", ""); +} + +TEST(CWDemangler, TestDemangleFunctionArgs) +{ + DoDemangleFunctionArgsTest("v", "void", ""); + DoDemangleFunctionArgsTest("b", "bool", ""); + DoDemangleFunctionArgsTest("RC9CVector3fUc_x", "const CVector3f&, unsigned char", "_x"); +} + +TEST(CWDemangler, TestDemangle) +{ + DoDemangleTest("__ct__11AbstractGfxFv", "AbstractGfx::AbstractGfx()"); + + DoDemangleTest("__dt__11AbstractGfxFv", "AbstractGfx::~AbstractGfx()"); + + DoDemangleTest( + "OnPrimitiveCommand__Q216@unnamed@code_c@20FifoPlaybackAnalyzerFQ316@unnamed@code_c@" + "13OpcodeDecoder9PrimitiveUcUlUsPUc", + "@unnamed@code_c@::FifoPlaybackAnalyzer::OnPrimitiveCommand(@unnamed@code_c@::OpcodeDecoder::" + "Primitive, unsigned char, unsigned long, unsigned short, unsigned char*)"); + + DoDemangleTest("SetSIMDMode__Q26Common3FPUFQ36Common3FPU9RoundModeb", + "Common::FPU::SetSIMDMode(Common::FPU::RoundMode, bool)"); + + DoDemangleTest("SupportsUtilityDrawing__11AbstractGfxCFv", + "AbstractGfx::SupportsUtilityDrawing() const"); + + DoDemangleTest("SetViewport__11AbstractGfxFffffff", + "AbstractGfx::SetViewport(float, float, float, float, float, float)"); + + DoDemangleTest("CreateTexture__11AbstractGfxFR13TextureConfigQ23std43basic_string_view>", + "AbstractGfx::CreateTexture(TextureConfig&, std::basic_string_view>)"); + + DoDemangleTest("SetViewportAndScissor__11AbstractGfxFRQ28MathUtil12Rectangleff", + "AbstractGfx::SetViewportAndScissor(MathUtil::Rectangle&, float, float)"); + + DoDemangleTest("GetInstance__18AchievementManagerFv", "AchievementManager::GetInstance()"); + + DoDemangleTest("FilterApprovedPatches__18AchievementManagerCFRQ23std70vector<" + "Q211PatchEngine5Patch,Q23std32allocator>RQ23std59basic_" + "string,Q23std12allocator>Us", + "AchievementManager::FilterApprovedPatches(std::vector>&, std::basic_string, std::allocator>&, unsigned short) const"); + + DoDemangleTest( + "__as__Q218AchievementManager15FilereaderStateFPQ218AchievementManager15FilereaderState", + "AchievementManager::FilereaderState::operator=(AchievementManager::FilereaderState*)"); + + DoDemangleTest("LeaderboardEntriesCallback__18AchievementManagerFiPcP34rc_client_leaderboard_" + "entry_list_tP11rc_client_tPv", + "AchievementManager::LeaderboardEntriesCallback(int, char*, " + "rc_client_leaderboard_entry_list_t*, rc_client_t*, void*)"); + + DoDemangleTest("Request__18AchievementManagerFP16rc_api_request_tPFP24rc_api_server_response_tPv_" + "vPvP11rc_client_t", + "AchievementManager::Request(rc_api_request_t*, void " + "(*)(rc_api_server_response_t*, void*), void*, rc_client_t*)"); + + DoDemangleTest("MemoryPeeker__18AchievementManagerFUlPUcUlP11rc_client_t", + "AchievementManager::MemoryPeeker(unsigned long, unsigned char*, unsigned long, " + "rc_client_t*)"); + + DoDemangleTest("Write__11BoundingBoxFUlQ23std11span", + "BoundingBox::Write(unsigned long, std::span)"); + + DoDemangleTest( + "__ct__24CachedInterpreterEmitterFPUcPUc", + "CachedInterpreterEmitter::CachedInterpreterEmitter(unsigned char*, unsigned char*)"); + + DoDemangleTest("Write__24CachedInterpreterEmitterFPFRQ27PowerPC12PowerPCStatePv_iPvUx", + "CachedInterpreterEmitter::Write(int (*)(PowerPC::PowerPCState&, void*), void*, " + "unsigned long long)"); + + DoDemangleTest("Shear__Q26Common8Matrix44Fff", "Common::Matrix44::Shear(float, float)"); + + DoDemangleTest("__opQ310WiimoteEmu10Shinkansen10DataFormat__Q26Common61BitCastPtrType<" + "Q310WiimoteEmu10Shinkansen10DataFormat,A21_Uc>Fv", + "Common::BitCastPtrType::operator WiimoteEmu::Shinkansen::DataFormat()"); + + DoDemangleTest( + "__ct__Q26Common61BitCastPtrTypeFPA21_Uc", + "Common::BitCastPtrType::BitCastPtrType(unsigned char(*)[21])"); + + DoDemangleTest("__pp__Q36Common10BitSet8IteratorFi", + "Common::BitSet::Iterator::operator++(int)"); + + DoDemangleTest("__ne__Q36Common10BitSet8IteratorFQ36Common10BitSet8Iterator", + "Common::BitSet::Iterator::operator!=(Common::BitSet::Iterator)"); + + DoDemangleTest("__lt__Q26Common10BitSetFQ26Common10BitSet", + "Common::BitSet::operator<(Common::BitSet)"); + + DoDemangleTest("__opb__Q26Common10BitSetFv", + "Common::BitSet::operator bool()"); + + DoDemangleTest("AddChildCodeSpace__Q26Common28CodeBlockFPQ26Common28CodeBlock<" + "Q23Gen8XEmitter,1>Ux", + "Common::CodeBlock::AddChildCodeSpace(Common::CodeBlock*, unsigned long long)"); + + DoDemangleTest( + "__ct__Q26Common505Lazy," + "Q23std12allocator>,Q23std59basic_string,Q23std12allocator>," + "Q23std73hash,Q23std12allocator>>," + "Q23std77equal_to,Q23std12allocator>>," + "Q23std162allocator," + "Q23std12allocator>,Q23std59basic_string,Q23std12allocator>>>>" + ">Fv", + "Common::Lazy, " + "std::allocator>, std::basic_string, " + "std::allocator>, std::hash, " + "std::allocator>>, std::equal_to, " + "std::allocator>>, std::allocator, std::allocator>, std::basic_string, std::allocator>>>>>::Lazy()"); + + DoDemangleTest( + "Append__Q26Common59LinearDiskCache<37ShaderUid<24geometry_shader_uid_data>,Uc>" + "FRC37ShaderUid<24geometry_shader_uid_data>PCUcUl", + "Common::LinearDiskCache, unsigned char>::Append(const " + "ShaderUid&, const unsigned char*, unsigned long)"); + + DoDemangleTest("SetIsWii__Q24Core6SystemFb", "Core::System::SetIsWii(bool)"); + + DoDemangleTest( + "SetDOLFromFile__Q26DiscIO22DirectoryBlobPartitionFRQ23std59basic_string,Q23std12allocator>UxPQ23std32vector>", + "DiscIO::DirectoryBlobPartition::SetDOLFromFile(std::basic_string, std::allocator>&, unsigned long long, std::vector>*)"); + + DoDemangleTest( + "SetDOL__Q26DiscIO22DirectoryBlobPartitionFQ26DiscIO14FSTBuilderNodeUxPQ23std32vector>", + "DiscIO::DirectoryBlobPartition::SetDOL(DiscIO::FSTBuilderNode, unsigned long long, " + "std::vector>*)"); + + DoDemangleTest("ReadFromGroups__Q26DiscIO19WIARVZFileReader<1>FPUxPUxPPUcUxUlUxUxUlUlUl", + "DiscIO::WIARVZFileReader<1>::ReadFromGroups(unsigned long long*, unsigned long " + "long*, unsigned char**, unsigned long long, unsigned long, unsigned long long, " + "unsigned long long, unsigned long, unsigned long, unsigned long)"); + + DoDemangleTest("GetCompanyFromID__6DiscIOFRCQ23std59basic_string," + "Q23std12allocator>", + "DiscIO::GetCompanyFromID(const std::basic_string, " + "std::allocator>&)"); + + DoDemangleTest("DiscordJoinRequest__Q27Discord7HandlerFPcRQ23std59basic_string,Q23std12allocator>Pc", + "Discord::Handler::DiscordJoinRequest(char*, std::basic_string, std::allocator>&, char*)"); + + DoDemangleTest("AddVoice__Q33DSP3HLE18ZeldaAudioRendererFUs", + "DSP::HLE::ZeldaAudioRenderer::AddVoice(unsigned short)"); + + DoDemangleTest("SetConstPatterns__Q33DSP3HLE18ZeldaAudioRendererFPQ23std12array", + "DSP::HLE::ZeldaAudioRenderer::SetConstPatterns(std::array*)"); + + DoDemangleTest("ReJitConditional__Q33DSP3x6410DSPEmitterFUsMQ33DSP3x6410DSPEmitterFPCvPvUs_v", + "DSP::x64::DSPEmitter::ReJitConditional(unsigned short, void " + "(DSP::x64::DSPEmitter::*)(unsigned short))"); + + DoDemangleTest("decode_mb__9ERContextFPviiiPA4_A2_Piiiii", + "ERContext::decode_mb(void*, int, int, int, int*(*)[4][2], int, int, int, int)"); + + DoDemangleTest("Write__Q24File12DirectIOFileFQ23std11span", + "File::DirectIOFile::Write(std::span)"); + + DoDemangleTest("SetBoth__Q27PowerPC12PairedSingleFff", + "PowerPC::PairedSingle::SetBoth(float, float)"); + + DoDemangleTest("GetPaletteConversionPipeline__Q211VideoCommon11ShaderCacheF10TLUTFormat", + "VideoCommon::ShaderCache::GetPaletteConversionPipeline(TLUTFormat)"); + + DoDemangleTest( + "GetGXPipelineConfig__" + "Q211VideoCommon11ShaderCacheFP18NativeVertexFormatP14AbstractShaderP14AbstractShaderP14Abstr" + "actShaderR18RasterizationStateR10DepthStateR13BlendingState21AbstractPipelineUsage", + "VideoCommon::ShaderCache::GetGXPipelineConfig(NativeVertexFormat*, AbstractShader*, " + "AbstractShader*, AbstractShader*, RasterizationState&, DepthState&, BlendingState&, " + "AbstractPipelineUsage)"); + + DoDemangleTest("LoadCaches__Q211VideoCommon11ShaderCacheFv", + "VideoCommon::ShaderCache::LoadCaches()"); + + DoDemangleTest( + "LoadPipelineCache>,b>,Q23std40less<" + "Q211VideoCommon17GXUberPipelineUid>,Q23std159allocator>,b>>>>>__" + "Q211VideoCommon11ShaderCacheFRQ23std355map>,b>," + "Q23std40less,Q23std159allocator>,b>>>>RQ26Common64LinearDiskCache<" + "Q211VideoCommon27SerializedGXUberPipelineUid,Uc>7APITypePCcb_v", + "void VideoCommon::ShaderCache::LoadPipelineCache>, bool>, " + "std::less, std::allocator>, bool>>>>>(std::map>, bool>, " + "std::less, std::allocator>, bool>>>>&, " + "Common::LinearDiskCache&, APIType, " + "const char*, bool)"); + + DoDemangleTest( + "BuildDesiredExtensionState__Q210WiimoteEmu7TaTaConFPQ210WiimoteEmu21DesiredExtensionState", + "WiimoteEmu::TaTaCon::BuildDesiredExtensionState(WiimoteEmu::DesiredExtensionState*)"); + + DoDemangleTest("__ct__Q210WiimoteEmu7WiimoteFUl", "WiimoteEmu::Wiimote::Wiimote(unsigned long)"); + + DoDemangleTest("SetWiimoteDeviceIndex__Q210WiimoteEmu7WiimoteFUc", + "WiimoteEmu::Wiimote::SetWiimoteDeviceIndex(unsigned char)"); + + DoDemangleTest("GetAngularVelocity__Q210WiimoteEmu7WiimoteFQ26Common8TVec3", + "WiimoteEmu::Wiimote::GetAngularVelocity(Common::TVec3)"); + + DoDemangleTest("BareFn__FPFPCcPv_v_v", "void BareFn(void (*)(const char*, void*))"); + DoDemangleTest("BareFn__FPFPCcPv_v_PFPCvPv_v", + "void (* BareFn(void (*)(const char*, void*)))(const void*, void*)"); + DoDemangleTest( + "SomeFn__FRCPFPFPCvPv_v_RCPFPCvPv_v", + "SomeFn(void (*const& (*const&)(void (*)(const void*, void*)))(const void*, void*))"); + DoDemangleTest( + "SomeFn__Q29Namespace5ClassCFRCMQ29Namespace5ClassFPCvPCvMQ29Namespace5ClassFPCvPCvPCvPv_" + "v_RCMQ29Namespace5ClassFPCvPCvPCvPv_v", + "Namespace::Class::SomeFn(void (Namespace::Class::*const & (Namespace::Class::*const " + "&)(void (Namespace::Class::*)(const void*, void*) const) const)(const void*, void*) " + "const) const"); + DoDemangleTest("Matrix__FfPA2_A3_f", "Matrix(float, float(*)[2][3])"); + DoDemangleTest("test__FRCPCPCi", "test(const int* const* const&)"); + + DoDemangleTest("@GUARD@GetCompanyFromID__6DiscIOFRCQ23std59basic_string," + "Q23std12allocator>@EMPTY_STRING", + "DiscIO::GetCompanyFromID(const std::basic_string, " + "std::allocator>&)::EMPTY_STRING guard"); + + DoDemangleTest("@LOCAL@GetCompanyFromID__6DiscIOFRCQ23std59basic_string," + "Q23std12allocator>@EMPTY_STRING", + "DiscIO::GetCompanyFromID(const std::basic_string, " + "std::allocator>&)::EMPTY_STRING"); + + DoDemangleTest("@LOCAL@SetSIMDMode__Q26Common3FPUFQ36Common3FPU9RoundModeb@EXCEPTION_MASK", + "Common::FPU::SetSIMDMode(Common::FPU::RoundMode, bool)::EXCEPTION_MASK"); + + DoDemangleTest("@LOCAL@GetInstance__18AchievementManagerFv@s_instance", + "AchievementManager::GetInstance()::s_instance"); + + DoDemangleTest("s_instance$localstatic3$GetInstance__18AchievementManagerFv", + "AchievementManager::GetInstance()::s_instance"); + + DoDemangleTest("init$localstatic4$GetInstance__18AchievementManagerFv", + "AchievementManager::GetInstance()::localstatic4 guard"); +} + +TEST(CWDemangler, TestDemangleOptions) +{ + DoDemangleOptionsTest(true, false, "__dt__Q210WiimoteEmu7WiimoteFv", + "WiimoteEmu::Wiimote::~Wiimote()"); + DoDemangleOptionsTest(false, false, "__dt__Q210WiimoteEmu7WiimoteFv", + "WiimoteEmu::Wiimote::~Wiimote(void)"); + DoDemangleOptionsTest(true, true, "example__10HahaOne<1>CFv", + "HahaOne<__int128>::example() const"); + DoDemangleOptionsTest(true, true, "fn<3,PV2>__FPC2", + "fn<3, volatile __vec2x32float__*>(const __vec2x32float__*)"); +} diff --git a/Source/UnitTests/UnitTests.vcxproj b/Source/UnitTests/UnitTests.vcxproj index 6b476dad9406..0cda54ff8846 100644 --- a/Source/UnitTests/UnitTests.vcxproj +++ b/Source/UnitTests/UnitTests.vcxproj @@ -47,6 +47,7 @@ + From 941419695858944b634eff9d2959c9188ce525e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Tue, 12 May 2026 09:48:52 +0200 Subject: [PATCH 031/270] Externals: Update SDL to release-3.4.8 This jumps us up two releases, both of which only introduce bug fixes. Changes potentially relevant to us are: [3.4.6](https://github.com/libsdl-org/SDL/tree/release-3.4.6): - Fixed Xbox controllers being unresponsive on preview release of Windows 11 - Fixed the menu button state on controllers on Apple platforms [3.4.8](https://github.com/libsdl-org/SDL/tree/release-3.4.8) (hotfix release): - Fixed a controller related crash on Windows, introduced in 3.4.6 --- Externals/SDL/SDL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/SDL/SDL b/Externals/SDL/SDL index 5848e584a1b6..d9d5536704d5 160000 --- a/Externals/SDL/SDL +++ b/Externals/SDL/SDL @@ -1 +1 @@ -Subproject commit 5848e584a1b606de26e3dbd1c7e4ecbc34f807a6 +Subproject commit d9d5536704d585616d4db3c8ba3c4ff6fc2757e1 From 0f72b69dc7a9e48d3e29f6ff28a032b91c23e0f6 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 4 Nov 2023 17:48:57 +0100 Subject: [PATCH 032/270] PPCAnalyst: Split "in use" analysis into reads and writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the last write to a register comes before the last read of it, we can write the register to ppcState after the last write instead of after the last read. This will hopefully help spread out m_ppc_state writes across a code block, improving pipelining. Also, if there's a conditional branch that's after the last write but before the last read, instead of needing to emit one m_ppc_state write on each side of the branch, we now only need to emit one m_ppc_state write. A note about the changes made to stmw and mfcr: These instructions don't write to any GPRs or CRs respectively – they only read from them. With this commit, there are no longer any cases where registers get written back to m_ppc_state after an instruction that just reads from them, so we can get rid of all STP logic from these two instructions. lmw still needs its STP logic, since that one does write to registers. --- Source/Core/Core/PowerPC/Jit64/Jit.cpp | 17 +++- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 5 +- .../PowerPC/Jit64/RegCache/GPRRegCache.cpp | 2 +- .../PowerPC/Jit64/RegCache/JitRegCache.cpp | 7 +- .../Core/PowerPC/Jit64/RegCache/JitRegCache.h | 7 +- Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 41 ++++---- .../PowerPC/JitArm64/JitArm64_BackPatch.cpp | 4 +- .../Core/PowerPC/JitArm64/JitArm64_Branch.cpp | 28 +++--- .../PowerPC/JitArm64/JitArm64_LoadStore.cpp | 93 ++++--------------- .../PowerPC/JitArm64/JitArm64_RegCache.cpp | 57 +++++++++--- .../Core/PowerPC/JitArm64/JitArm64_RegCache.h | 46 ++++----- .../JitArm64/JitArm64_SystemRegisters.cpp | 19 ++-- Source/Core/Core/PowerPC/PPCAnalyst.cpp | 39 +++++--- Source/Core/Core/PowerPC/PPCAnalyst.h | 9 +- 14 files changed, 189 insertions(+), 185 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index 0b976eb1c8b9..1248b1ab01ea 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -351,8 +351,10 @@ void Jit64::Shutdown() void Jit64::FallBackToInterpreter(UGeckoInstruction inst) { FlushCarry(); - gpr.Flush(BitSet32(0xFFFFFFFF), RegCache::IgnoreDiscardedRegisters::Yes); - fpr.Flush(BitSet32(0xFFFFFFFF), RegCache::IgnoreDiscardedRegisters::Yes); + gpr.Flush(BitSet32(0xFFFFFFFF), RegCache::FlushMode::Full, + RegCache::IgnoreDiscardedRegisters::Yes); + fpr.Flush(BitSet32(0xFFFFFFFF), RegCache::FlushMode::Full, + RegCache::IgnoreDiscardedRegisters::Yes); if (js.op->canEndBlock) { @@ -1151,7 +1153,8 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) // output, which needs to be bound in the actual instruction compilation. // TODO: make this smarter in the case that we're actually register-starved, i.e. // prioritize the more important registers. - gpr.PreloadRegisters(op.regsIn & op.gprInUse & ~op.gprDiscardable); + gpr.PreloadRegisters(op.regsIn & (op.gprWillBeRead | op.gprWillBeWritten) & + ~op.gprDiscardable); fpr.PreloadRegisters(op.fregsIn & op.fprInXmm & ~op.fprDiscardable); } @@ -1230,8 +1233,12 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) gpr.Discard(op.gprDiscardable); fpr.Discard(op.fprDiscardable); } - gpr.Flush(~op.gprInUse & (op.regsIn | op.regsOut)); - fpr.Flush(~op.fprInUse & (op.fregsIn | op.GetFregsOut())); + gpr.Flush(~(op.gprWillBeRead | op.gprWillBeWritten) & (op.regsIn | op.regsOut), + RegCache::FlushMode::Full); + fpr.Flush(~(op.fprWillBeRead | op.fprWillBeWritten) & (op.fregsIn | op.GetFregsOut()), + RegCache::FlushMode::Full); + gpr.Flush(~op.gprWillBeWritten & op.regsOut, RegCache::FlushMode::Undirty); + fpr.Flush(~op.fprWillBeWritten & op.GetFregsOut(), RegCache::FlushMode::Undirty); if (opinfo->flags & FL_LOADSTORE) ++js.numLoadStoreInst; diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index f3c329cef4c7..70f61d8b9a11 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -194,7 +194,10 @@ void Jit64::ComputeRC(preg_t preg, bool needs_test, bool needs_sext) // We don't want to do this if a test is needed though, because it would interrupt macro-op // fusion. arg.Unlock(); - gpr.Flush(~js.op->gprInUse); + gpr.Flush(~(js.op->gprWillBeRead | js.op->gprWillBeWritten) & + (js.op->regsIn | js.op->regsOut), + RegCache::FlushMode::Full); + gpr.Flush(~js.op->gprWillBeWritten & js.op->regsOut, RegCache::FlushMode::Undirty); } DoMergedBranchCondition(); } diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp index a740d76e3dc9..fdbe13e192cb 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/GPRRegCache.cpp @@ -115,7 +115,7 @@ void GPRRegCache::SetImmediate32(preg_t preg, u32 imm_value, bool dirty) BitSet32 GPRRegCache::GetRegUtilization() const { - return m_jit.js.op->gprInUse; + return m_jit.js.op->gprWillBeRead | m_jit.js.op->gprWillBeWritten; } BitSet32 GPRRegCache::CountRegsIn(preg_t preg, u32 lookahead) const diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp index 55c3a3aceabc..592813011d4b 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp @@ -330,7 +330,8 @@ void RegCache::Discard(BitSet32 pregs) } } -void RegCache::Flush(BitSet32 pregs, IgnoreDiscardedRegisters ignore_discarded_registers) +void RegCache::Flush(BitSet32 pregs, FlushMode mode, + IgnoreDiscardedRegisters ignore_discarded_registers) { ASSERT_MSG(DYNA_REC, std::ranges::none_of(m_xregs, &X64CachedReg::IsLocked), "Someone forgot to unlock a X64 reg"); @@ -342,7 +343,7 @@ void RegCache::Flush(BitSet32 pregs, IgnoreDiscardedRegisters ignore_discarded_r ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Register transaction is in progress for {}!", i); - StoreFromRegister(i, FlushMode::Full, ignore_discarded_registers); + StoreFromRegister(i, mode, ignore_discarded_registers); } } @@ -615,7 +616,7 @@ void RegCache::Realize(preg_t preg) if (m_constraints[preg].ShouldBeRevertable()) { - StoreFromRegister(preg, FlushMode::MaintainState); + StoreFromRegister(preg, FlushMode::Undirty); do_bind(); m_regs[preg].SetRevertable(); return; diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h index 41bdeb08c3f9..7a692a2b6309 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.h @@ -121,8 +121,11 @@ class RegCache public: enum class FlushMode { + // All dirty registers get written back, and all registers get removed from the cache. Full, - MaintainState, + // All dirty registers get written back and get set as no longer dirty. + // No registers are removed from the cache. + Undirty, }; enum class IgnoreDiscardedRegisters @@ -175,7 +178,7 @@ class RegCache RCForkGuard Fork(); void Discard(BitSet32 pregs); - void Flush(BitSet32 pregs = BitSet32::AllTrue(32), + void Flush(BitSet32 pregs = BitSet32::AllTrue(32), FlushMode mode = FlushMode::Full, IgnoreDiscardedRegisters ignore_discarded_registers = IgnoreDiscardedRegisters::No); void Reset(BitSet32 pregs); void Revert(); diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index 5b61528fae86..aba973346d39 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -257,8 +257,8 @@ void JitArm64::Shutdown() void JitArm64::FallBackToInterpreter(UGeckoInstruction inst) { FlushCarry(); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG, IgnoreDiscardedRegisters::Yes); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG, IgnoreDiscardedRegisters::Yes); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG, IgnoreDiscardedRegisters::Yes); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG, IgnoreDiscardedRegisters::Yes); if (js.op->canEndBlock) { @@ -322,8 +322,8 @@ void JitArm64::FallBackToInterpreter(UGeckoInstruction inst) void JitArm64::HLEFunction(u32 hook_index) { FlushCarry(); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); ABI_CallFunction(&HLE::ExecuteFromJIT, js.compilerPC, hook_index, &m_system); } @@ -456,8 +456,8 @@ void JitArm64::MSRUpdated(u32 msr) // Call PageTableUpdatedFromJit if needed if (UReg_MSR(msr).DR) { - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); auto WA = gpr.GetScopedReg(); @@ -495,8 +495,8 @@ void JitArm64::MSRUpdated(ARM64Reg msr) // Call PageTableUpdatedFromJit if needed MOV(WA, msr); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); FixupBranch dr_unset = TBZ(WA, u8(UReg_MSR{}.DR.StartBit())); static_assert(PPCSTATE_OFF(pagetable_update_pending) < 0x1000); LDRB(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF(pagetable_update_pending)); @@ -1309,8 +1309,8 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) m_system.GetPowerPC().GetBreakPoints().IsAddressBreakPoint(op.address)) { FlushCarry(); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); static_assert(PPCSTATE_OFF(pc) <= 252); static_assert(PPCSTATE_OFF(pc) + 4 == PPCSTATE_OFF(npc)); @@ -1369,8 +1369,8 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) if (bJITRegisterCacheOff) { FlushCarry(); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); m_constant_propagation.Clear(); CompileInstruction(op); @@ -1416,9 +1416,16 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) fpr.DiscardRegisters(op.fprDiscardable); gpr.DiscardCRRegisters(op.crDiscardable); } - gpr.StoreRegisters(~op.gprInUse & (op.regsIn | op.regsOut)); - fpr.StoreRegisters(~op.fprInUse & (op.fregsIn | op.GetFregsOut())); - gpr.StoreCRRegisters(~op.crInUse & (op.crIn | op.crOut)); + gpr.FlushRegisters(~(op.gprWillBeRead | op.gprWillBeWritten) & (op.regsIn | op.regsOut), + FlushMode::Full); + fpr.FlushRegisters(~(op.fprWillBeRead | op.fprWillBeWritten) & + (op.fregsIn | op.GetFregsOut()), + FlushMode::Full); + gpr.FlushCRRegisters(~(op.crWillBeRead | op.crWillBeWritten) & (op.crIn | op.crOut), + FlushMode::Full); + gpr.FlushRegisters(~op.gprWillBeWritten & op.regsOut, FlushMode::Undirty); + fpr.FlushRegisters(~op.fprWillBeWritten & op.GetFregsOut(), FlushMode::Undirty); + gpr.FlushCRRegisters(~op.crWillBeWritten & op.crOut, FlushMode::Undirty); if (opinfo->flags & FL_LOADSTORE) ++js.numLoadStoreInst; @@ -1433,8 +1440,8 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) if (code_block.m_broken) { - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); WriteExit(nextPC); } diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp index 0c11dcbc3af5..8368a7cc9138 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp @@ -322,8 +322,8 @@ void JitArm64::FlushPPCStateBeforeSlowAccess(ARM64Reg temp_gpr, ARM64Reg temp_fp MemChecks& mem_checks = m_system.GetPowerPC().GetMemChecks(); if (mem_checks.HasAny()) { - gpr.StoreRegisters(mem_checks.GetGPRsUsedInConditions(), temp_gpr, FlushMode::MaintainState); - fpr.StoreRegisters(mem_checks.GetFPRsUsedInConditions(), temp_fpr, FlushMode::MaintainState); + gpr.FlushRegisters(mem_checks.GetGPRsUsedInConditions(), FlushMode::MaintainState, temp_gpr); + fpr.FlushRegisters(mem_checks.GetFPRsUsedInConditions(), FlushMode::MaintainState, temp_fpr); } } diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp index 191c42387689..8f391538c584 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp @@ -21,8 +21,8 @@ void JitArm64::sc(UGeckoInstruction inst) INSTRUCTION_START JITDISABLE(bJITBranchOff); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); { auto WA = gpr.GetScopedReg(); @@ -39,8 +39,8 @@ void JitArm64::rfi(UGeckoInstruction inst) INSTRUCTION_START JITDISABLE(bJITBranchOff); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); // See Interpreter rfi for details const u32 mask = 0x87C0FFFF; @@ -140,8 +140,8 @@ void JitArm64::bx(UGeckoInstruction inst) return; } - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); if (js.op->branchIsIdleLoop) { @@ -243,8 +243,8 @@ void JitArm64::bcx(UGeckoInstruction inst) if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE)) { - gpr.Flush(FlushMode::All, WA); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, WA); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); if (IsBranchWatchEnabled()) { WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, {}, {}); @@ -275,8 +275,8 @@ void JitArm64::bcctrx(UGeckoInstruction inst) // BO_2 == 1z1zz -> b always // NPC = CTR & 0xfffffffc; - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); Arm64GPRCache::ScopedARM64Reg WB = ARM64Reg::INVALID_REG; if (inst.LK_3) @@ -345,8 +345,8 @@ void JitArm64::bclrx(UGeckoInstruction inst) STR(IndexType::Unsigned, WB, PPC_REG, PPCSTATE_OFF_SPR(SPR_LR)); } - gpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, WB); - fpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::Full, WB); + fpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::Full, ARM64Reg::INVALID_REG); if (IsBranchWatchEnabled()) { @@ -390,8 +390,8 @@ void JitArm64::bclrx(UGeckoInstruction inst) if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE)) { - gpr.Flush(FlushMode::All, WA); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, WA); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); if (IsBranchWatchEnabled()) { WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, {}, {}); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp index 8c80151e91e5..13e98aae79ad 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp @@ -559,26 +559,11 @@ void JitArm64::lmw(UGeckoInstruction inst) } } - BitSet32 gprs_to_flush = ~js.op->gprInUse & BitSet32(0xFFFFFFFFU << d); - if (!js.op->gprInUse[a]) - { - if (!a_is_addr_base_reg) - { - gprs_to_flush[a] = true; - } - else - { - gprs_to_flush[a] = false; + BitSet32 gprs_to_undirty = ~js.op->gprWillBeWritten & BitSet32(0xFFFFFFFFU << d); - if (a + 1 == d && (std::countr_one((~js.op->gprInUse).m_val >> a) & 1) == 0) - { - // In this situation, we can save one store instruction by flushing GPR d together with GPR - // a, but we shouldn't flush GPR a until the end of the PPC instruction. Therefore, let's - // also wait with flushing GPR d until the end of the PPC instruction. - gprs_to_flush[d] = false; - } - } - } + BitSet32 gprs_to_flush = ~(js.op->gprWillBeWritten | js.op->gprWillBeRead); + if (a_is_addr_base_reg) + gprs_to_flush[a] = false; // TODO: This doesn't handle rollback on DSI correctly constexpr u32 flags = BackPatchInfo::FLAG_LOAD | BackPatchInfo::FLAG_SIZE_32; @@ -614,18 +599,20 @@ void JitArm64::lmw(UGeckoInstruction inst) { gpr.DiscardRegisters(BitSet32{int(i)}); } - else if (gprs_to_flush[i]) + else if (gprs_to_undirty[i]) { - BitSet32 gprs_to_flush_this_time{}; - if (i != 0 && gprs_to_flush[i - 1]) - gprs_to_flush_this_time = BitSet32{int(i - 1), int(i)}; - else if (i == 31 || !gprs_to_flush[i + 1]) - gprs_to_flush_this_time = BitSet32{int(i)}; + BitSet32 gprs_to_undirty_this_time{}; + if (i != 0 && gprs_to_undirty[i - 1]) + gprs_to_undirty_this_time = BitSet32{int(i - 1), int(i)}; + else if (i == 31 || !gprs_to_undirty[i + 1]) + gprs_to_undirty_this_time = BitSet32{int(i)}; else continue; - gpr.StoreRegisters(gprs_to_flush_this_time); - gprs_to_flush &= ~gprs_to_flush_this_time; + gpr.FlushRegisters(gprs_to_undirty_this_time, FlushMode::Undirty, ARM64Reg::INVALID_REG); + gpr.FlushRegisters(gprs_to_undirty_this_time & gprs_to_flush, FlushMode::Full, + ARM64Reg::INVALID_REG); + gprs_to_undirty &= ~gprs_to_undirty_this_time; } } @@ -677,27 +664,7 @@ void JitArm64::stmw(UGeckoInstruction inst) } } - const BitSet32 dirty_gprs_to_flush_unmasked = ~js.op->gprInUse & gpr.GetDirtyGPRs(); - BitSet32 dirty_gprs_to_flush = dirty_gprs_to_flush_unmasked & BitSet32(0xFFFFFFFFU << s); - if (dirty_gprs_to_flush_unmasked[a]) - { - if (!a_is_addr_base_reg) - { - dirty_gprs_to_flush[a] = true; - } - else - { - dirty_gprs_to_flush[a] = false; - - if (a + 1 == s && (std::countr_one((~js.op->gprInUse).m_val >> a) & 1) == 0) - { - // In this situation, we can save one store instruction by flushing GPR s together with GPR - // a, but we shouldn't flush GPR a until the end of the PPC instruction. Therefore, let's - // also wait with flushing GPR s until the end of the PPC instruction. - dirty_gprs_to_flush[s] = false; - } - } - } + const BitSet32 gprs_to_flush = ~(js.op->gprWillBeRead | js.op->gprWillBeWritten); // TODO: This doesn't handle rollback on DSI correctly constexpr u32 flags = BackPatchInfo::FLAG_STORE | BackPatchInfo::FLAG_SIZE_32; @@ -720,34 +687,12 @@ void JitArm64::stmw(UGeckoInstruction inst) EmitBackpatchRoutine(flags, MemAccessMode::Auto, src_reg, EncodeRegTo64(addr_reg), regs_in_use, fprs_in_use); - // To reduce register pressure and to avoid getting a pipeline-unfriendly long run of stores - // after this instruction, flush registers that would be flushed after this instruction anyway. - // - // We try to store two registers at a time when possible to let the register cache use STP. + // To reduce register pressure, flush registers that would be flushed after this instruction + // anyway. if (gprs_to_discard[i]) - { gpr.DiscardRegisters(BitSet32{int(i)}); - } - else if (dirty_gprs_to_flush[i]) - { - BitSet32 gprs_to_flush_this_time{}; - if (i != 0 && dirty_gprs_to_flush[i - 1]) - gprs_to_flush_this_time = BitSet32{int(i - 1), int(i)}; - else if (i == 31 || !dirty_gprs_to_flush[i + 1]) - gprs_to_flush_this_time = BitSet32{int(i)}; - else - continue; - - gpr.StoreRegisters(gprs_to_flush_this_time); - dirty_gprs_to_flush &= ~gprs_to_flush_this_time; - } - else if (!js.op->gprInUse[i]) - { - // If this register can be flushed but it isn't dirty, no store instruction will be emitted - // when flushing it, so it doesn't matter if we flush it together with another register or - // not. Let's just flush it in the simplest way possible. - gpr.StoreRegisters(BitSet32{int(i)}); - } + else if (gprs_to_flush[i]) + gpr.FlushRegisters(BitSet32{int(i)}, FlushMode::Full, ARM64Reg::INVALID_REG); } gpr.Unlock(ARM64Reg::W1, ARM64Reg::W2, ARM64Reg::W30); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp index 308c14fddefe..de967cf85a04 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.cpp @@ -121,7 +121,7 @@ void Arm64RegCache::FlushMostStaleRegister() } } - FlushRegister(most_stale_preg, FlushMode::All, ARM64Reg::INVALID_REG); + FlushRegister(most_stale_preg, FlushMode::Full, ARM64Reg::INVALID_REG); } void Arm64RegCache::DiscardRegister(size_t preg) @@ -203,11 +203,15 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg if (!reg.IsInPPCState()) m_emit->STR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset)); - if (mode == FlushMode::All) + if (mode == FlushMode::Full) { UnlockRegister(EncodeRegTo32(host_reg)); reg.Flush(); } + else if (mode == FlushMode::Undirty) + { + reg.SetDirty(false); + } } else if (is_gpr && IsImm(index - GUEST_GPR_OFFSET)) { @@ -244,8 +248,10 @@ void Arm64GPRCache::FlushRegister(size_t index, FlushMode mode, ARM64Reg tmp_reg } } - if (mode == FlushMode::All) + if (mode == FlushMode::Full) reg.Flush(); + else if (mode == FlushMode::Undirty) + reg.SetDirty(false); } } @@ -270,10 +276,10 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r const bool reg2_imm = IsImm(i + 1); const bool reg1_zero = reg1_imm && GetImm(i) == 0; const bool reg2_zero = reg2_imm && GetImm(i + 1) == 0; - const bool flush_all = mode == FlushMode::All; + const bool can_allocate_reg = mode != FlushMode::MaintainState; if (!reg1.IsInPPCState() && !reg2.IsInPPCState() && - (reg1.IsInHostRegister() || (reg1_imm && (reg1_zero || flush_all))) && - (reg2.IsInHostRegister() || (reg2_imm && (reg2_zero || flush_all)))) + (reg1.IsInHostRegister() || (reg1_imm && (reg1_zero || can_allocate_reg))) && + (reg2.IsInHostRegister() || (reg2_imm && (reg2_zero || can_allocate_reg)))) { const size_t ppc_offset = GetGuestByIndex(i).ppc_offset; if (ppc_offset <= 252) @@ -281,7 +287,7 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r ARM64Reg RX1 = reg1_zero ? ARM64Reg::WZR : BindForRead(i); ARM64Reg RX2 = reg2_zero ? ARM64Reg::WZR : BindForRead(i + 1); m_emit->STP(IndexType::Signed, RX1, RX2, PPC_REG, u32(ppc_offset)); - if (flush_all) + if (mode == FlushMode::Full) { if (reg1.IsInHostRegister()) UnlockRegister(reg1.GetReg()); @@ -290,6 +296,11 @@ void Arm64GPRCache::FlushRegisters(BitSet32 regs, FlushMode mode, ARM64Reg tmp_r reg1.Flush(); reg2.Flush(); } + else if (mode == FlushMode::Undirty) + { + reg1.SetDirty(false); + reg2.SetDirty(false); + } ++iter; continue; } @@ -497,7 +508,7 @@ void Arm64GPRCache::FlushByHost(ARM64Reg host_reg, ARM64Reg tmp_reg) const OpArg& reg = m_guest_registers[i]; if (reg.IsInHostRegister() && DecodeReg(reg.GetReg()) == DecodeReg(host_reg)) { - FlushRegister(i, FlushMode::All, tmp_reg); + FlushRegister(i, FlushMode::Full, tmp_reg); return; } } @@ -788,7 +799,7 @@ void Arm64FPRCache::FlushByHost(ARM64Reg host_reg, ARM64Reg tmp_reg) if (reg.IsInHostRegister() && reg.GetReg() == host_reg) { - FlushRegister(i, FlushMode::All, tmp_reg); + FlushRegister(i, FlushMode::Full, tmp_reg); return; } } @@ -817,6 +828,22 @@ void Arm64FPRCache::FlushRegister(size_t preg, FlushMode mode, ARM64Reg tmp_reg) const bool dirty = !reg.IsInPPCState(); RegType type = reg.GetFPRType(); + if (mode == FlushMode::Undirty) + { + switch (type) + { + case RegType::Single: + case RegType::DuplicatedSingle: + case RegType::LowerPairSingle: + // In this situation, skip flushing. It's usually better to wait until later instead to avoid + // extra conversions. We can revisit this decision in the future if the register cache gets + // the ability to store both the single and double versions of a value simultaneously. + return; + default: + break; + } + } + bool allocated_tmp_reg = false; if (tmp_reg != ARM64Reg::INVALID_REG) { @@ -868,11 +895,15 @@ void Arm64FPRCache::FlushRegister(size_t preg, FlushMode mode, ARM64Reg tmp_reg) static_cast(PPCSTATE_OFF_PS0(preg))); } - if (mode == FlushMode::All) + if (mode == FlushMode::Full) { UnlockRegister(host_reg); reg.Flush(); } + else if (mode == FlushMode::Undirty) + { + reg.SetDirty(false); + } } else if (type == RegType::Duplicated) { @@ -892,11 +923,15 @@ void Arm64FPRCache::FlushRegister(size_t preg, FlushMode mode, ARM64Reg tmp_reg) } } - if (mode == FlushMode::All) + if (mode == FlushMode::Full) { UnlockRegister(host_reg); reg.Flush(); } + else if (mode == FlushMode::Undirty) + { + reg.SetDirty(false); + } } if (allocated_tmp_reg) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h index cde4fbe6cb21..7f16630acdd9 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_RegCache.h @@ -68,13 +68,17 @@ enum class RegType DuplicatedSingle, // PS0 and PS1 are identical, host register only stores one lane (32-bit) }; -enum class FlushMode : bool +enum class FlushMode { - // Flushes all registers, no exceptions - All, - // Flushes registers in a conditional branch - // Doesn't wipe the state of the registers from the cache + // All dirty registers get written back, and all registers get removed from the cache. + Full, + // All dirty registers get written back, but the state of the cache is untouched. + // The host registers may get clobbered. This is intended for use when doing a block exit + // after a conditional branch. MaintainState, + // Most dirty registers get written back and get set as no longer dirty. + // No registers are removed from the cache. + Undirty, }; enum class IgnoreDiscardedRegisters @@ -379,17 +383,15 @@ class Arm64GPRCache : public Arm64RegCache BitSet32 GetDirtyGPRs() const; - void StoreRegisters(BitSet32 regs, Arm64Gen::ARM64Reg tmp_reg = Arm64Gen::ARM64Reg::INVALID_REG, - FlushMode flush_mode = FlushMode::All) - { - FlushRegisters(regs, flush_mode, tmp_reg, IgnoreDiscardedRegisters::No); - } + void FlushRegisters( + BitSet32 regs, FlushMode flush_mode = FlushMode::Full, + Arm64Gen::ARM64Reg tmp_reg = Arm64Gen::ARM64Reg::INVALID_REG, + IgnoreDiscardedRegisters ignore_discarded_registers = IgnoreDiscardedRegisters::No); - void StoreCRRegisters(BitSet8 regs, Arm64Gen::ARM64Reg tmp_reg = Arm64Gen::ARM64Reg::INVALID_REG, - FlushMode flush_mode = FlushMode::All) - { - FlushCRRegisters(regs, flush_mode, tmp_reg, IgnoreDiscardedRegisters::No); - } + void FlushCRRegisters( + BitSet8 regs, FlushMode flush_mode = FlushMode::Full, + Arm64Gen::ARM64Reg tmp_reg = Arm64Gen::ARM64Reg::INVALID_REG, + IgnoreDiscardedRegisters ignore_discarded_registers = IgnoreDiscardedRegisters::No); void DiscardCRRegisters(BitSet8 regs); void ResetCRRegisters(BitSet8 regs); @@ -436,11 +438,6 @@ class Arm64GPRCache : public Arm64RegCache void SetImmediateInternal(size_t index, u32 imm, bool dirty); void BindForWrite(size_t index, bool will_read, bool will_write = true); - void FlushRegisters(BitSet32 regs, FlushMode mode, Arm64Gen::ARM64Reg tmp_reg, - IgnoreDiscardedRegisters ignore_discarded_registers); - void FlushCRRegisters(BitSet8 regs, FlushMode mode, Arm64Gen::ARM64Reg tmp_reg, - IgnoreDiscardedRegisters ignore_discarded_registers); - static constexpr size_t GUEST_GPR_COUNT = 32; static constexpr size_t GUEST_CR_COUNT = 8; static constexpr size_t GUEST_GPR_OFFSET = 0; @@ -470,11 +467,8 @@ class Arm64FPRCache : public Arm64RegCache void FixSinglePrecision(size_t preg); - void StoreRegisters(BitSet32 regs, Arm64Gen::ARM64Reg tmp_reg = Arm64Gen::ARM64Reg::INVALID_REG, - FlushMode flush_mode = FlushMode::All) - { - FlushRegisters(regs, flush_mode, tmp_reg); - } + void FlushRegisters(BitSet32 regs, FlushMode flush_mode = FlushMode::Full, + Arm64Gen::ARM64Reg tmp_reg = Arm64Gen::ARM64Reg::INVALID_REG); protected: // Get the order of the host registers @@ -489,6 +483,4 @@ class Arm64FPRCache : public Arm64RegCache private: bool IsCallerSaved(Arm64Gen::ARM64Reg reg) const; bool IsTopHalfUsed(Arm64Gen::ARM64Reg reg) const; - - void FlushRegisters(BitSet32 regs, FlushMode mode, Arm64Gen::ARM64Reg tmp_reg); }; diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp index 21a0003887ad..30f80f4dff5a 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp @@ -227,8 +227,8 @@ void JitArm64::mtmsr(UGeckoInstruction inst) if (!imm_value) MSRUpdated(gpr.R(inst.RS)); - gpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); WriteExceptionExit(js.compilerPC + 4, true); } @@ -367,8 +367,8 @@ void JitArm64::twx(UGeckoInstruction inst) if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE)) { - gpr.Flush(FlushMode::All, WA); - fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + gpr.Flush(FlushMode::Full, WA); + fpr.Flush(FlushMode::Full, ARM64Reg::INVALID_REG); WriteExit(js.compilerPC + 4); } } @@ -714,15 +714,12 @@ void JitArm64::mfcr(UGeckoInstruction inst) CMP(CR, ARM64Reg::ZR); CSEL(WA, WC, WA, CC_GT); - // To reduce register pressure and to avoid getting a pipeline-unfriendly long run of stores - // after this instruction, flush registers that would be flushed after this instruction anyway. - // - // There's no point in ensuring we flush two registers at the same time, because the offset in - // ppcState for CRs is too large to be encoded into an STP instruction. + // To reduce register pressure, flush registers that would be flushed after this instruction + // anyway. if (js.op->crDiscardable[i]) gpr.DiscardCRRegisters(BitSet8{i}); - else if (!js.op->crInUse[i]) - gpr.StoreCRRegisters(BitSet8{i}, WC); + else if (!(js.op->crWillBeRead | js.op->crWillBeWritten)[i]) + gpr.FlushCRRegisters(BitSet8{i}, FlushMode::Full, WC); } } diff --git a/Source/Core/Core/PowerPC/PPCAnalyst.cpp b/Source/Core/Core/PowerPC/PPCAnalyst.cpp index 40b974b0d8f1..6ecb9c326297 100644 --- a/Source/Core/Core/PowerPC/PPCAnalyst.cpp +++ b/Source/Core/Core/PowerPC/PPCAnalyst.cpp @@ -984,8 +984,9 @@ u32 PPCAnalyzer::Analyze(u32 address, CodeBlock* block, CodeBuffer* buffer, // wants flags, to be safe. bool wantsFPRF = true; bool wantsCA = true; - BitSet8 crInUse, crDiscardable; - BitSet32 gprBlockInputs, gprInUse, fprInUse, gprDiscardable, fprDiscardable, fprInXmm; + BitSet8 crWillBeRead, crWillBeWritten, crDiscardable; + BitSet32 gprWillBeRead, gprWillBeWritten, fprWillBeRead, fprWillBeWritten, gprDiscardable, + fprDiscardable, fprInXmm; for (int i = block->m_num_instructions - 1; i >= 0; i--) { CodeOp& op = code[i]; @@ -1012,28 +1013,38 @@ u32 PPCAnalyzer::Analyze(u32 address, CodeBlock* block, CodeBuffer* buffer, wantsCA |= opWantsCA || may_exit_block; wantsFPRF &= !op.outputFPRF || opWantsFPRF; wantsCA &= !op.outputCA || opWantsCA; - op.gprInUse = gprInUse; - op.fprInUse = fprInUse; - op.crInUse = crInUse; + op.gprWillBeRead = gprWillBeRead; + op.gprWillBeWritten = gprWillBeWritten; + op.fprWillBeRead = fprWillBeRead; + op.fprWillBeWritten = fprWillBeWritten; + op.crWillBeRead = crWillBeRead; + op.crWillBeWritten = crWillBeWritten; op.gprDiscardable = gprDiscardable; op.fprDiscardable = fprDiscardable; op.crDiscardable = crDiscardable; op.fprInXmm = fprInXmm; - gprBlockInputs &= ~op.regsOut; - gprBlockInputs |= op.regsIn; - gprInUse |= op.regsIn | op.regsOut; - fprInUse |= op.fregsIn | op.GetFregsOut(); - crInUse |= op.crIn | op.crOut; + gprWillBeRead &= ~op.regsOut; + gprWillBeRead |= op.regsIn; + gprWillBeWritten |= op.regsOut; + fprWillBeRead &= ~op.GetFregsOut(); + fprWillBeRead |= op.fregsIn; + fprWillBeWritten |= op.GetFregsOut(); + crWillBeRead &= ~op.crOut; + crWillBeRead |= op.crIn; + crWillBeWritten |= op.crOut; if (strncmp(op.opinfo->opname, "stfd", 4)) fprInXmm |= op.fregsIn; if (hle || breakpoint) { - gprInUse = BitSet32{}; - fprInUse = BitSet32{}; + gprWillBeRead = BitSet32{}; + gprWillBeWritten = BitSet32{}; + fprWillBeRead = BitSet32{}; + fprWillBeWritten = BitSet32{}; fprInXmm = BitSet32{}; - crInUse = BitSet8{}; + crWillBeRead = BitSet8{}; + crWillBeWritten = BitSet8{}; gprDiscardable = BitSet32{}; fprDiscardable = BitSet32{}; crDiscardable = BitSet8{}; @@ -1149,7 +1160,7 @@ u32 PPCAnalyzer::Analyze(u32 address, CodeBlock* block, CodeBuffer* buffer, } block->m_gqr_used = gqrUsed; block->m_gqr_modified = gqrModified; - block->m_gpr_inputs = gprBlockInputs; + block->m_gpr_inputs = gprWillBeRead; return address; } diff --git a/Source/Core/Core/PowerPC/PPCAnalyst.h b/Source/Core/Core/PowerPC/PPCAnalyst.h index 879bd06111c8..f9081bae9336 100644 --- a/Source/Core/Core/PowerPC/PPCAnalyst.h +++ b/Source/Core/Core/PowerPC/PPCAnalyst.h @@ -51,11 +51,14 @@ struct CodeOp // 16B bool canCauseException = false; bool skipLRStack = false; bool skip = false; // followed BL-s for example - BitSet8 crInUse; + BitSet8 crWillBeRead; + BitSet8 crWillBeWritten; BitSet8 crDiscardable; // which registers are still needed after this instruction in this block - BitSet32 fprInUse; - BitSet32 gprInUse; + BitSet32 gprWillBeRead; + BitSet32 gprWillBeWritten; + BitSet32 fprWillBeRead; + BitSet32 fprWillBeWritten; // which registers have values which are known to be unused after this instruction BitSet32 gprDiscardable; BitSet32 fprDiscardable; From 21d44f32bad3a384396efafc612d74ef6f5eff8e Mon Sep 17 00:00:00 2001 From: DacoTaco Date: Wed, 13 May 2026 20:26:47 +0200 Subject: [PATCH 033/270] fixes: make dolreader validate section addresses and sizes IOS and IPL reject non-32byte aligned sections --- Source/Core/Core/Boot/DolReader.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Core/Core/Boot/DolReader.cpp b/Source/Core/Core/Boot/DolReader.cpp index 0ec828c5191c..aa5f3dae0d65 100644 --- a/Source/Core/Core/Boot/DolReader.cpp +++ b/Source/Core/Core/Boot/DolReader.cpp @@ -53,6 +53,14 @@ bool DolReader::Initialize(std::span buffer) { if (m_dolheader.textSize[i] != 0) { + if ((m_dolheader.textAddress[i] & 31) != 0 || (m_dolheader.textSize[i] & 31) != 0) + { + ERROR_LOG_FMT(BOOT, + "Text section {} is not 32-byte aligned: address = 0x{:08x}, size = 0x{:x}", + i, m_dolheader.textAddress[i], m_dolheader.textSize[i]); + return false; + } + if (buffer.size() < m_dolheader.textOffset[i] + m_dolheader.textSize[i]) return false; @@ -80,6 +88,14 @@ bool DolReader::Initialize(std::span buffer) { u32 section_size = m_dolheader.dataSize[i]; u32 section_offset = m_dolheader.dataOffset[i]; + if ((m_dolheader.dataAddress[i] & 31) != 0 || (section_size & 31) != 0) + { + ERROR_LOG_FMT(BOOT, + "Data section {} is not 32-byte aligned: address = 0x{:08x}, size = 0x{:x}", + i, m_dolheader.dataAddress[i], section_size); + return false; + } + if (buffer.size() < section_offset) return false; From c716c32ce891b340f5b287f6ae589e43832c427e Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 6 Apr 2026 13:32:57 +0100 Subject: [PATCH 034/270] Add compose dependencies for Android and empty NetplaySetupActivity Derive compose colour theming from the existing XML styles already set at the activity level. --- Source/Android/app/build.gradle.kts | 11 ++ .../Android/app/src/main/AndroidManifest.xml | 5 + .../netplay/ui/NetplaySetupActivity.kt | 107 ++++++++++++++++++ .../dolphinemu/ui/main/MainPresenter.kt | 6 + .../dolphinemu/ui/theme/DolphinTheme.kt | 103 +++++++++++++++++ .../app/src/main/res/menu/menu_game_grid.xml | 5 + .../app/src/main/res/values/strings.xml | 5 + Source/Android/build.gradle.kts | 1 + Source/Android/gradle/libs.versions.toml | 9 ++ 9 files changed, 252 insertions(+) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 0a952a070403..82196ff97b61 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.androidx.baselineprofile) } @@ -10,6 +11,7 @@ android { ndkVersion = "29.0.14206865" buildFeatures { + compose = true viewBinding = true buildConfig = true resValues = true @@ -150,6 +152,15 @@ dependencies { implementation(libs.kotlinx.coroutines.android) implementation(libs.filepicker) + + // Jetpack Compose + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.material.icons) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.tooling.preview) } fun getGitVersion(): String { diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 3218ed3ed89c..e68fa9f7578f 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -133,6 +133,11 @@ android:label="@string/user_data_submenu" android:theme="@style/Theme.Dolphin.Main" /> + + Unit, +) { + Scaffold( + topBar = { + MediumTopAppBar( + title = { Text(stringResource(R.string.netplay_setup_title)) }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .consumeWindowInsets(innerPadding) + .padding(innerPadding) + .padding(horizontal = DolphinTheme.scaffoldPadding) + .verticalScroll(rememberScrollState()), + ) { + } + } +} + +@Preview +@Composable +private fun NetplaySetupScreenPreview() { + MaterialTheme { + NetplayScreen(onBackClicked = {}) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt index 893d6a16d4d7..1e1d55dbd7de 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt @@ -21,6 +21,7 @@ import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemMenuNotInstalledDialogFragment import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateProgressBarDialogFragment import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateViewModel +import org.dolphinemu.dolphinemu.features.netplay.ui.NetplaySetupActivity import org.dolphinemu.dolphinemu.fragments.AboutDialogFragment import org.dolphinemu.dolphinemu.model.GameFileCache import org.dolphinemu.dolphinemu.services.GameFileCacheManager @@ -188,6 +189,11 @@ class MainPresenter(private val mainView: MainView, private val activity: Fragme true } + R.id.menu_netplay -> { + NetplaySetupActivity.launch(activity) + true + } + R.id.menu_about -> { showAboutDialog() false diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt new file mode 100644 index 000000000000..58eb52b34790 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.theme + +import android.content.Context +import androidx.annotation.AttrRes +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.google.android.material.color.MaterialColors +import androidx.appcompat.R as AppCompatR +import com.google.android.material.R as MaterialR + +object DolphinTheme { + val scaffoldPadding = 16.dp +} + +@Composable +fun DolphinTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val isDark = isSystemInDarkTheme() + val colorScheme = remember(context, isDark) { context.toDolphinColorScheme(isDark) } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} + +private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme { + fun attr(@AttrRes attr: Int) = Color(MaterialColors.getColor(this, attr, 0)) + + val background = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)).use { + Color(it.getColor(0, 0)) + } + + return if (isDark) { + darkColorScheme( + primary = attr(AppCompatR.attr.colorPrimary), + onPrimary = attr(MaterialR.attr.colorOnPrimary), + primaryContainer = attr(MaterialR.attr.colorPrimaryContainer), + onPrimaryContainer = attr(MaterialR.attr.colorOnPrimaryContainer), + secondary = attr(MaterialR.attr.colorSecondary), + onSecondary = attr(MaterialR.attr.colorOnSecondary), + secondaryContainer = attr(MaterialR.attr.colorSecondaryContainer), + onSecondaryContainer = attr(MaterialR.attr.colorOnSecondaryContainer), + tertiary = attr(MaterialR.attr.colorTertiary), + onTertiary = attr(MaterialR.attr.colorOnTertiary), + tertiaryContainer = attr(MaterialR.attr.colorTertiaryContainer), + onTertiaryContainer = attr(MaterialR.attr.colorOnTertiaryContainer), + error = attr(AppCompatR.attr.colorError), + onError = attr(MaterialR.attr.colorOnError), + errorContainer = attr(MaterialR.attr.colorErrorContainer), + onErrorContainer = attr(MaterialR.attr.colorOnErrorContainer), + background = background, + onBackground = attr(MaterialR.attr.colorOnBackground), + surface = attr(MaterialR.attr.colorSurface), + onSurface = attr(MaterialR.attr.colorOnSurface), + surfaceVariant = attr(MaterialR.attr.colorSurfaceVariant), + onSurfaceVariant = attr(MaterialR.attr.colorOnSurfaceVariant), + outline = attr(MaterialR.attr.colorOutline), + inverseSurface = attr(MaterialR.attr.colorSurfaceInverse), + inverseOnSurface = attr(MaterialR.attr.colorOnSurfaceInverse), + inversePrimary = attr(MaterialR.attr.colorPrimaryInverse), + ) + } else { + lightColorScheme( + primary = attr(AppCompatR.attr.colorPrimary), + onPrimary = attr(MaterialR.attr.colorOnPrimary), + primaryContainer = attr(MaterialR.attr.colorPrimaryContainer), + onPrimaryContainer = attr(MaterialR.attr.colorOnPrimaryContainer), + secondary = attr(MaterialR.attr.colorSecondary), + onSecondary = attr(MaterialR.attr.colorOnSecondary), + secondaryContainer = attr(MaterialR.attr.colorSecondaryContainer), + onSecondaryContainer = attr(MaterialR.attr.colorOnSecondaryContainer), + tertiary = attr(MaterialR.attr.colorTertiary), + onTertiary = attr(MaterialR.attr.colorOnTertiary), + tertiaryContainer = attr(MaterialR.attr.colorTertiaryContainer), + onTertiaryContainer = attr(MaterialR.attr.colorOnTertiaryContainer), + error = attr(AppCompatR.attr.colorError), + onError = attr(MaterialR.attr.colorOnError), + errorContainer = attr(MaterialR.attr.colorErrorContainer), + onErrorContainer = attr(MaterialR.attr.colorOnErrorContainer), + background = background, + onBackground = attr(MaterialR.attr.colorOnBackground), + surface = attr(MaterialR.attr.colorSurface), + onSurface = attr(MaterialR.attr.colorOnSurface), + surfaceVariant = attr(MaterialR.attr.colorSurfaceVariant), + onSurfaceVariant = attr(MaterialR.attr.colorOnSurfaceVariant), + outline = attr(MaterialR.attr.colorOutline), + inverseSurface = attr(MaterialR.attr.colorSurfaceInverse), + inverseOnSurface = attr(MaterialR.attr.colorOnSurfaceInverse), + inversePrimary = attr(MaterialR.attr.colorPrimaryInverse), + ) + } +} diff --git a/Source/Android/app/src/main/res/menu/menu_game_grid.xml b/Source/Android/app/src/main/res/menu/menu_game_grid.xml index 2a282198d128..c52c574c445c 100644 --- a/Source/Android/app/src/main/res/menu/menu_game_grid.xml +++ b/Source/Android/app/src/main/res/menu/menu_game_grid.xml @@ -51,6 +51,11 @@ android:title="@string/grid_menu_online_system_update" app:showAsAction="never"/> + + Import Wii Save Import BootMii NAND Backup Perform Online System Update + Netplay Load Wii System Menu Load Wii System Menu (%s) Load vWii System Menu (%s) @@ -980,4 +981,8 @@ It can efficiently compress both junk data and encrypted Wii data. Log Out Logging In Login Failed + + + Netplay Setup + Nickname diff --git a/Source/Android/build.gradle.kts b/Source/Android/build.gradle.kts index 5682b2f13050..c74d069b447a 100644 --- a/Source/Android/build.gradle.kts +++ b/Source/Android/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.android.test) apply false alias(libs.plugins.androidx.baselineprofile) apply false + alias(libs.plugins.kotlin.compose) apply false } buildscript { diff --git a/Source/Android/gradle/libs.versions.toml b/Source/Android/gradle/libs.versions.toml index 09fa7b09caff..e29e0c2e1042 100644 --- a/Source/Android/gradle/libs.versions.toml +++ b/Source/Android/gradle/libs.versions.toml @@ -4,6 +4,7 @@ appcompat = "1.7.1" benchmarkMacroJunit4 = "1.5.0-alpha04" cardview = "1.0.0" coil = "2.7.0" +compose-bom = "2025.04.00" constraintlayout = "2.2.1" coreKtx = "1.18.0" coreSplashscreen = "1.2.0" @@ -27,9 +28,16 @@ tvprovider = "1.1.0" uiautomator = "2.3.0" [libraries] +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } androidx-cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } @@ -58,4 +66,5 @@ android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } android-test = { id = "com.android.test", version.ref = "agp" } androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmarkMacroJunit4" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } From f10022f2dc6e5e7d9636d3e91fe1dc3737dc703e Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 15 Apr 2026 17:33:09 +0200 Subject: [PATCH 035/270] Netplay setup UI Only for connecting, no hosting yet. --- Source/Android/app/build.gradle.kts | 6 + .../features/netplay/model/ConnectionRole.kt | 16 ++ .../features/netplay/model/ConnectionType.kt | 21 ++ .../netplay/model/NetplaySetupViewModel.kt | 58 +++++ .../netplay/ui/NetplaySetupActivity.kt | 240 +++++++++++++++++- .../dolphinemu/ui/theme/DolphinTheme.kt | 6 + .../app/src/main/res/values/strings.xml | 8 + 7 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 82196ff97b61..912906d2d88d 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -118,6 +118,12 @@ android { } } +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xannotation-default-target=param-property") + } +} + dependencies { baselineProfile(project(":benchmark")) coreLibraryDesugaring(libs.desugar.jdk.libs) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt new file mode 100644 index 000000000000..a54d08256607 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt @@ -0,0 +1,16 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +import androidx.annotation.StringRes +import org.dolphinemu.dolphinemu.R + +sealed class ConnectionRole( + @StringRes val labelId: Int, +) { + object Connect : ConnectionRole(R.string.netplay_connection_role_connect) + object Host : ConnectionRole(R.string.netplay_connection_role_host) + + companion object { + val all: List + get() = listOf(Connect, Host) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt new file mode 100644 index 000000000000..aed03f89cc38 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt @@ -0,0 +1,21 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +import androidx.annotation.StringRes +import org.dolphinemu.dolphinemu.R + +sealed class ConnectionType( + @StringRes val labelId: Int, +) { + object DirectConnection : ConnectionType( + labelId = R.string.netplay_connection_type_direct_connection, + ) + + object TraversalServer : ConnectionType( + labelId = R.string.netplay_connection_type_traversal_server, + ) + + companion object { + val all: List + get() = listOf(DirectConnection, TraversalServer) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt new file mode 100644 index 000000000000..8d0dd05c79b6 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay.model + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class NetplaySetupViewModel : ViewModel() { + private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) + val connectionRole = _connectionRole.asStateFlow() + + private val _nickname = MutableStateFlow("") + val nickname = _nickname.asStateFlow() + + private val _connectionType = MutableStateFlow(ConnectionType.DirectConnection) + val connectionType = _connectionType.asStateFlow() + + private val _ipAddress = MutableStateFlow("") + val ipAddress = _ipAddress.asStateFlow() + + private val _hostCode = MutableStateFlow("") + val hostCode = _hostCode.asStateFlow() + + private val _connectPort = MutableStateFlow(0.toString()) + val connectPort = _connectPort.asStateFlow() + + fun setConnectionRole(connectionRole: ConnectionRole) { + _connectionRole.value = connectionRole + } + + fun setNickname(nickname: String) { + _nickname.value = nickname + } + + fun setConnectionType(connectionType: ConnectionType) { + _connectionType.value = connectionType + } + + fun setIpAddress(ipAddress: String) { + if (ipAddress.all { it.isDigit() || it == '.' }) { + _ipAddress.value = ipAddress + } + } + + fun setHostCode(hostCode: String) { + _hostCode.value = hostCode + } + + fun setConnectPort(port: String) { + if (port.all { it.isDigit() }) { + _connectPort.value = port + } + } + + fun connect() { + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index f79584c484c7..71d83ddf8191 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -12,39 +12,77 @@ import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SecondaryTabRow +import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.ViewModelProvider import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole +import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType +import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme +import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.utils.ThemeHelper class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { override var themeId: Int = 0 + private lateinit var viewModel: NetplaySetupViewModel override fun onCreate(savedInstanceState: Bundle?) { ThemeHelper.setTheme(this) enableEdgeToEdge() super.onCreate(savedInstanceState) + viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] + setContent { DolphinTheme { NetplaySetupScreen( onBackClicked = { finish() }, + nickname = viewModel.nickname.collectAsState().value, + onNicknameChanged = viewModel::setNickname, + connectionType = viewModel.connectionType.collectAsState().value, + onConnectionTypeChanged = viewModel::setConnectionType, + connectionRole = viewModel.connectionRole.collectAsState().value, + onConnectionRoleChanged = viewModel::setConnectionRole, + ipAddress = viewModel.ipAddress.collectAsState().value, + onIpAddressChanged = viewModel::setIpAddress, + connectPort = viewModel.connectPort.collectAsState().value, + onConnectPortChanged = viewModel::setConnectPort, + hostCode = viewModel.hostCode.collectAsState().value, + onHostCodeChanged = viewModel::setHostCode, + onConnectClicked = viewModel::connect, ) } } @@ -71,6 +109,19 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { @Composable private fun NetplaySetupScreen( onBackClicked: () -> Unit, + connectionRole: ConnectionRole, + onConnectionRoleChanged: (ConnectionRole) -> Unit, + nickname: String, + onNicknameChanged: (String) -> Unit, + connectionType: ConnectionType, + onConnectionTypeChanged: (ConnectionType) -> Unit, + ipAddress: String, + onIpAddressChanged: (String) -> Unit, + connectPort: String, + onConnectPortChanged: (String) -> Unit, + hostCode: String, + onHostCodeChanged: (String) -> Unit, + onConnectClicked: () -> Unit, ) { Scaffold( topBar = { @@ -85,16 +136,184 @@ private fun NetplaySetupScreen( } }, ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = onConnectClicked, + ) { + Text(stringResource(connectionRole.labelId)) + } } ) { innerPadding -> Column( modifier = Modifier + .fillMaxSize() .consumeWindowInsets(innerPadding) + .verticalScroll(rememberScrollState()) .padding(innerPadding) - .padding(horizontal = DolphinTheme.scaffoldPadding) - .verticalScroll(rememberScrollState()), ) { + SecondaryTabRow(selectedTabIndex = ConnectionRole.all.indexOf(connectionRole)) { + ConnectionRole.all.forEach { role -> + Tab( + selected = connectionRole == role, + onClick = { onConnectionRoleChanged(role) }, + text = { Text(stringResource(role.labelId)) }, + ) + } + } + + MenuSpacer() + + Column( + modifier = Modifier + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) { + NetplaySetupContent( + nickname = nickname, + onNicknameChanged = onNicknameChanged, + connectionType = connectionType, + onConnectionTypeChanged = onConnectionTypeChanged, + ipAddress = ipAddress, + onIpAddressChanged = onIpAddressChanged, + hostCode = hostCode, + onHostCodeChanged = onHostCodeChanged, + connectPort = connectPort, + onConnectPortChanged = onConnectPortChanged, + connectionRole = connectionRole, + ) + } + } + } +} + +@Composable +private fun NetplaySetupContent( + nickname: String, + onNicknameChanged: (String) -> Unit, + connectionType: ConnectionType, + onConnectionTypeChanged: (ConnectionType) -> Unit, + ipAddress: String, + onIpAddressChanged: (String) -> Unit, + hostCode: String, + onHostCodeChanged: (String) -> Unit, + connectPort: String, + onConnectPortChanged: (String) -> Unit, + connectionRole: ConnectionRole, +) { + OutlinedTextField( + value = nickname, + onValueChange = onNicknameChanged, + label = { Text(stringResource(R.string.netplay_nickname_label)) }, + modifier = Modifier.fillMaxWidth() + ) + + MenuSpacer() + + ConnectionTypePicker( + connectionType = connectionType, + onConnectionTypeChanged = onConnectionTypeChanged, + ) + + MenuSpacer() + + when (connectionRole) { + ConnectionRole.Connect -> ConnectMenu( + connectionType = connectionType, + ipAddress = ipAddress, + onIpAddressChanged = onIpAddressChanged, + hostCode = hostCode, + onHostCodeChanged = onHostCodeChanged, + port = connectPort, + onPortChanged = onConnectPortChanged, + ) + + ConnectionRole.Host -> {} + } +} + +@Composable +private fun ConnectionTypePicker( + connectionType: ConnectionType, + onConnectionTypeChanged: (ConnectionType) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + ConnectionType.all.forEach { connectionType -> + DropdownMenuItem( + text = { Text(stringResource(connectionType.labelId)) }, + onClick = { + onConnectionTypeChanged(connectionType) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + OutlinedTextField( + value = stringResource(connectionType.labelId), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.netplay_connection_type)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth() + ) + } +} + +@Composable +fun ConnectMenu( + connectionType: ConnectionType, + ipAddress: String, + onIpAddressChanged: (String) -> Unit, + hostCode: String, + onHostCodeChanged: (String) -> Unit, + port: String, + onPortChanged: (String) -> Unit, +) { + when (connectionType) { + ConnectionType.DirectConnection -> { + OutlinedTextField( + value = ipAddress, + onValueChange = onIpAddressChanged, + label = { Text(stringResource(R.string.netplay_ip_address_label)) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + modifier = Modifier + .fillMaxWidth() + ) + + MenuSpacer() + + OutlinedTextField( + value = port, + onValueChange = onPortChanged, + label = { Text(stringResource(R.string.netplay_port_label)) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + modifier = Modifier + .fillMaxWidth() + ) } + + ConnectionType.TraversalServer -> OutlinedTextField( + value = hostCode, + onValueChange = onHostCodeChanged, + label = { Text(stringResource(R.string.netplay_host_code_label)) }, + modifier = Modifier + .fillMaxWidth() + ) } } @@ -102,6 +321,21 @@ private fun NetplaySetupScreen( @Composable private fun NetplaySetupScreenPreview() { MaterialTheme { - NetplayScreen(onBackClicked = {}) + NetplaySetupScreen( + onBackClicked = {}, + nickname = "Preview nickname", + onNicknameChanged = {}, + connectionType = ConnectionType.DirectConnection, + onConnectionTypeChanged = {}, + connectionRole = ConnectionRole.Connect, + onConnectionRoleChanged = {}, + ipAddress = "127.0.0.1", + onIpAddressChanged = {}, + connectPort = "2626", + onConnectPortChanged = {}, + hostCode = "", + onHostCodeChanged = {}, + onConnectClicked = {}, + ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index 58eb52b34790..24b0f99c0aba 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -5,12 +5,15 @@ package org.dolphinemu.dolphinemu.ui.theme import android.content.Context import androidx.annotation.AttrRes import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -101,3 +104,6 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme { ) } } + +@Composable +fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp)) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 712d4744ccb0..78c9c3b34cb5 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -985,4 +985,12 @@ It can efficiently compress both junk data and encrypted Wii data. Netplay Setup Nickname + Connection type + Direct connection + Traversal server + Connect + Host + IP address + Host code + Port From a1983e5fa2f66911b7151e0fc8f90a86b7cece52 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 8 Apr 2026 13:33:42 +0100 Subject: [PATCH 036/270] Add Netplay settings JNI layer and wire up NetplaySetupViewModel --- .../dolphinemu/features/netplay/Netplay.kt | 96 ++++++++++++++ .../features/netplay/model/ConnectionType.kt | 3 + .../netplay/model/NetplaySetupViewModel.kt | 18 ++- Source/Android/jni/CMakeLists.txt | 1 + Source/Android/jni/NetPlay/Netplay.cpp | 120 ++++++++++++++++++ 5 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt create mode 100644 Source/Android/jni/NetPlay/Netplay.cpp diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt new file mode 100644 index 000000000000..ace7277a9a16 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -0,0 +1,96 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay + +import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType + +object Netplay { + @JvmStatic + external fun getNickname(): String + + fun getConnectionType(): ConnectionType = ConnectionType.all + .find { it.configValue == getTraversalChoice() } ?: throw IllegalStateException() + + @JvmStatic + external fun getTraversalChoice(): String + + @JvmStatic + external fun getAddress(): String + + @JvmStatic + external fun getHostCode(): String + + @JvmStatic + external fun getConnectPort(): Int + + @JvmStatic + external fun getHostPort(): Int + + @JvmStatic + external fun getUseUpnp(): Boolean + + @JvmStatic + external fun getEnableChunkedUploadLimit(): Boolean + + @JvmStatic + external fun getChunkedUploadLimit(): Int + + @JvmStatic + external fun getUseIndex(): Boolean + + @JvmStatic + external fun getIndexRegion(): String + + @JvmStatic + external fun getIndexName(): String + + @JvmStatic + external fun getIndexPassword(): String + + fun saveSetup( + nickname: String, + connectionType: ConnectionType, + address: String, + hostCode: String, + connectPort: Int, + ) { + SaveSetup( + nickname = nickname, + traversalChoice = connectionType.configValue, + address = address, + hostCode = hostCode, + connectPort = connectPort, + hostPort = 2626, + useUpnp = false, + useListenPort = false, + listenPort = 2626, + enableChunkedUploadLimit = false, + chunkedUploadLimit = 3000, + useIndex = false, + indexRegion = "", + indexName = "", + indexPassword = "", + ) + } + + @JvmStatic + external fun SaveSetup( + nickname: String, + traversalChoice: String, + address: String, + hostCode: String, + connectPort: Int, + hostPort: Int, + useUpnp: Boolean, + useListenPort: Boolean, + listenPort: Int, + enableChunkedUploadLimit: Boolean, + chunkedUploadLimit: Int, + useIndex: Boolean, + indexRegion: String, + indexName: String, + indexPassword: String, + ) + +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt index aed03f89cc38..f6b3f0eca15c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt @@ -5,13 +5,16 @@ import org.dolphinemu.dolphinemu.R sealed class ConnectionType( @StringRes val labelId: Int, + val configValue: String, ) { object DirectConnection : ConnectionType( labelId = R.string.netplay_connection_type_direct_connection, + configValue = "direct", ) object TraversalServer : ConnectionType( labelId = R.string.netplay_connection_type_traversal_server, + configValue = "traversal", ) companion object { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 8d0dd05c79b6..ecf0b2cff2f0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -5,24 +5,25 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import org.dolphinemu.dolphinemu.features.netplay.Netplay class NetplaySetupViewModel : ViewModel() { private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) val connectionRole = _connectionRole.asStateFlow() - private val _nickname = MutableStateFlow("") + private val _nickname = MutableStateFlow(Netplay.getNickname()) val nickname = _nickname.asStateFlow() - private val _connectionType = MutableStateFlow(ConnectionType.DirectConnection) + private val _connectionType = MutableStateFlow(Netplay.getConnectionType()) val connectionType = _connectionType.asStateFlow() - private val _ipAddress = MutableStateFlow("") + private val _ipAddress = MutableStateFlow(Netplay.getAddress()) val ipAddress = _ipAddress.asStateFlow() - private val _hostCode = MutableStateFlow("") + private val _hostCode = MutableStateFlow(Netplay.getHostCode()) val hostCode = _hostCode.asStateFlow() - private val _connectPort = MutableStateFlow(0.toString()) + private val _connectPort = MutableStateFlow(Netplay.getConnectPort().toString()) val connectPort = _connectPort.asStateFlow() fun setConnectionRole(connectionRole: ConnectionRole) { @@ -54,5 +55,12 @@ class NetplaySetupViewModel : ViewModel() { } fun connect() { + Netplay.saveSetup( + nickname = nickname.value, + connectionType = connectionType.value, + address = ipAddress.value, + hostCode = hostCode.value, + connectPort = connectPort.value.toInt(), + ) } } diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index a15eb5c63618..a71668a758f8 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(main SHARED Input/MappingCommon.cpp Input/NumericSetting.cpp Input/NumericSetting.h + NetPlay/Netplay.cpp MainAndroid.cpp RiivolutionPatches.cpp SkylanderConfig.cpp diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp new file mode 100644 index 000000000000..245a24852c75 --- /dev/null +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -0,0 +1,120 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#include "Common/CommonTypes.h" +#include "Core/Config/NetplaySettings.h" + +#include "jni/AndroidCommon/AndroidCommon.h" + +extern "C" { + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getNickname(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_NICKNAME)); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getTraversalChoice(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE)); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getAddress(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_ADDRESS)); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getHostCode(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_HOST_CODE)); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getConnectPort(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_CONNECT_PORT)); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getHostPort(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_HOST_PORT)); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getUseUpnp(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_USE_UPNP)); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getEnableChunkedUploadLimit(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_ENABLE_CHUNKED_UPLOAD_LIMIT)); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getChunkedUploadLimit(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_CHUNKED_UPLOAD_LIMIT)); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getUseIndex(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_USE_INDEX)); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexRegion(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_INDEX_REGION)); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexName(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_INDEX_NAME)); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexPassword(JNIEnv* env, jclass) +{ + return ToJString(env, Config::Get(Config::NETPLAY_INDEX_PASSWORD)); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup( + JNIEnv* env, jclass, jstring jnickname, jstring traversalChoice, jstring jaddress, + jstring jhostCode, jint connectPort, jint hostPort, jboolean useUpnp, jboolean useListenPort, + jint listenPort, jboolean enableChunkedUploadLimit, jint chunkedUploadLimit, jboolean useIndex, + jstring jindexRegion, jstring jindexName, jstring jindexPassword) +{ + Config::ConfigChangeCallbackGuard config_guard; + + Config::SetBaseOrCurrent(Config::NETPLAY_NICKNAME, GetJString(env, jnickname)); + Config::SetBaseOrCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, GetJString(env, traversalChoice)); + Config::SetBaseOrCurrent(Config::NETPLAY_ADDRESS, GetJString(env, jaddress)); + Config::SetBaseOrCurrent(Config::NETPLAY_HOST_CODE, GetJString(env, jhostCode)); + Config::SetBaseOrCurrent(Config::NETPLAY_CONNECT_PORT, static_cast(connectPort)); + Config::SetBaseOrCurrent(Config::NETPLAY_HOST_PORT, static_cast(hostPort)); + Config::SetBaseOrCurrent(Config::NETPLAY_USE_UPNP, static_cast(useUpnp)); + Config::SetBaseOrCurrent(Config::NETPLAY_ENABLE_CHUNKED_UPLOAD_LIMIT, + static_cast(enableChunkedUploadLimit)); + Config::SetBaseOrCurrent(Config::NETPLAY_CHUNKED_UPLOAD_LIMIT, + static_cast(chunkedUploadLimit)); + Config::SetBaseOrCurrent(Config::NETPLAY_USE_INDEX, static_cast(useIndex)); + Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_REGION, GetJString(env, jindexRegion)); + Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_NAME, GetJString(env, jindexName)); + Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_PASSWORD, GetJString(env, jindexPassword)); + Config::SetBaseOrCurrent(Config::NETPLAY_LISTEN_PORT, static_cast(listenPort)); +} + +} // extern "C" From 1464fdfc2845817b52c204b719e16975e5182fe5 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 6 May 2026 13:35:34 +0200 Subject: [PATCH 037/270] Add NetPlayClient join and stub NetPlayUICallbacks --- .../dolphinemu/features/netplay/Netplay.kt | 10 ++++ .../netplay/model/NetplaySetupViewModel.kt | 1 + Source/Android/jni/CMakeLists.txt | 2 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 53 +++++++++++++++++ .../Android/jni/NetPlay/NetPlayUICallbacks.h | 57 +++++++++++++++++++ Source/Android/jni/NetPlay/Netplay.cpp | 25 ++++++++ 6 files changed, 148 insertions(+) create mode 100644 Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp create mode 100644 Source/Android/jni/NetPlay/NetPlayUICallbacks.h diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index ace7277a9a16..aaaca2b4fb22 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -3,9 +3,13 @@ package org.dolphinemu.dolphinemu.features.netplay +import androidx.annotation.Keep import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType object Netplay { + @Keep + private var netPlayClientPointer: Long = 0 + @JvmStatic external fun getNickname(): String @@ -93,4 +97,10 @@ object Netplay { indexPassword: String, ) + fun join() { + netPlayClientPointer = Join() + } + + @JvmStatic + private external fun Join(): Long } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index ecf0b2cff2f0..34cee1e5bfd6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -62,5 +62,6 @@ class NetplaySetupViewModel : ViewModel() { hostCode = hostCode.value, connectPort = connectPort.value.toInt(), ) + Netplay.join() } } diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index a71668a758f8..a3a741a433fb 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -31,6 +31,8 @@ add_library(main SHARED Input/NumericSetting.cpp Input/NumericSetting.h NetPlay/Netplay.cpp + NetPlay/NetPlayUICallbacks.cpp + NetPlay/NetPlayUICallbacks.h MainAndroid.cpp RiivolutionPatches.cpp SkylanderConfig.cpp diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp new file mode 100644 index 000000000000..ab56458eff3b --- /dev/null +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -0,0 +1,53 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "jni/NetPlay/NetPlayUICallbacks.h" + +namespace NetPlay { + +NetPlayUICallbacks::NetPlayUICallbacks() = default; +NetPlayUICallbacks::~NetPlayUICallbacks() = default; + +void NetPlayUICallbacks::BootGame(const std::string&, std::unique_ptr) {} +void NetPlayUICallbacks::StopGame() {} +bool NetPlayUICallbacks::IsHosting() const { return false; } +void NetPlayUICallbacks::Update() {} +void NetPlayUICallbacks::AppendChat(const std::string&) {} +void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier&, const std::string&) {} +void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} +void NetPlayUICallbacks::OnMsgStartGame() {} +void NetPlayUICallbacks::OnMsgStopGame() {} +void NetPlayUICallbacks::OnMsgPowerButton() {} +void NetPlayUICallbacks::OnPlayerConnect(const std::string&) {} +void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} +void NetPlayUICallbacks::OnPadBufferChanged(u32) {} +void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool) {} +void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} +void NetPlayUICallbacks::OnConnectionLost() {} +void NetPlayUICallbacks::OnConnectionError(const std::string&) {} +void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {} +void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State) {} +void NetPlayUICallbacks::OnGameStartAborted() {} +void NetPlayUICallbacks::OnGolferChanged(bool, const std::string&) {} +void NetPlayUICallbacks::OnTtlDetermined(u8) {} +void NetPlayUICallbacks::OnIndexAdded(bool, std::string) {} +void NetPlayUICallbacks::OnIndexRefreshFailed(std::string) {} +bool NetPlayUICallbacks::IsRecording() { return false; } + +std::shared_ptr +NetPlayUICallbacks::FindGameFile(const NetPlay::SyncIdentifier&, NetPlay::SyncIdentifierComparison*) +{ + return nullptr; +} + +std::string NetPlayUICallbacks::FindGBARomPath(const std::array&, std::string_view, int) { return {}; } +void NetPlayUICallbacks::ShowGameDigestDialog(const std::string&) {} +void NetPlayUICallbacks::SetGameDigestProgress(int, int) {} +void NetPlayUICallbacks::SetGameDigestResult(int, const std::string&) {} +void NetPlayUICallbacks::AbortGameDigest() {} +void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string&, u64, std::span) {} +void NetPlayUICallbacks::HideChunkedProgressDialog() {} +void NetPlayUICallbacks::SetChunkedProgress(int, u64) {} +void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) {} + +} // namespace NetPlay diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h new file mode 100644 index 000000000000..18fdf1df93c3 --- /dev/null +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include "Core/NetPlayClient.h" + +namespace NetPlay { + +class NetPlayUICallbacks : public NetPlay::NetPlayUI { +public: + NetPlayUICallbacks(); + ~NetPlayUICallbacks() override; + + void BootGame(const std::string& filename, + std::unique_ptr boot_session_data) override; + void StopGame() override; + bool IsHosting() const override; + void Update() override; + void AppendChat(const std::string& msg) override; + void OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, + const std::string& netplay_name) override; + void OnMsgChangeGBARom(int pad, const NetPlay::GBAConfig& config) override; + void OnMsgStartGame() override; + void OnMsgStopGame() override; + void OnMsgPowerButton() override; + void OnPlayerConnect(const std::string& player) override; + void OnPlayerDisconnect(const std::string& player) override; + void OnPadBufferChanged(u32 buffer) override; + void OnHostInputAuthorityChanged(bool enabled) override; + void OnDesync(u32 frame, const std::string& player) override; + void OnConnectionLost() override; + void OnConnectionError(const std::string& message) override; + void OnTraversalError(Common::TraversalClient::FailureReason error) override; + void OnTraversalStateChanged(Common::TraversalClient::State state) override; + void OnGameStartAborted() override; + void OnGolferChanged(bool is_golfer, const std::string& golfer_name) override; + void OnTtlDetermined(u8 ttl) override; + void OnIndexAdded(bool success, std::string error) override; + void OnIndexRefreshFailed(std::string error) override; + bool IsRecording() override; + std::shared_ptr + FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, + NetPlay::SyncIdentifierComparison* found = nullptr) override; + std::string FindGBARomPath(const std::array& hash, std::string_view title, + int device_number) override; + void ShowGameDigestDialog(const std::string& title) override; + void SetGameDigestProgress(int pid, int progress) override; + void SetGameDigestResult(int pid, const std::string& result) override; + void AbortGameDigest() override; + void ShowChunkedProgressDialog(const std::string& title, u64 data_size, + std::span players) override; + void HideChunkedProgressDialog() override; + void SetChunkedProgress(int pid, u64 progress) override; + void SetHostWiiSyncData(std::vector titles, std::string redirect_folder) override; +}; + +} // namespace NetPlay diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 245a24852c75..42ed6483cebb 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -1,14 +1,17 @@ // Copyright 2003 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include "Common/CommonTypes.h" #include "Core/Config/NetplaySettings.h" +#include "Core/NetPlayClient.h" #include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/NetPlay/NetPlayUICallbacks.h" extern "C" { @@ -117,4 +120,26 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup( Config::SetBaseOrCurrent(Config::NETPLAY_LISTEN_PORT, static_cast(listenPort)); } +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv*, jclass) +{ + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + const bool is_traversal = traversal_choice == "traversal"; + + std::string host_ip; + host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : + Config::Get(Config::NETPLAY_ADDRESS); + + const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); + const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); + const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); + + auto* client = new NetPlay::NetPlayClient( + host_ip, host_port, new NetPlay::NetPlayUICallbacks(), nickname, + NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port}); + + return reinterpret_cast(client); +} + } // extern "C" From 766374434c56bb8d77a5676870a6779b2ee1799f Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 6 May 2026 13:44:53 +0200 Subject: [PATCH 038/270] Pass game list to NetPlayUICallbacks and implement OnMsgChangeGame, OnMsgStartGame, FindGameFile --- .../netplay/model/NetplaySetupViewModel.kt | 9 ++++ Source/Android/jni/AndroidCommon/IDCache.cpp | 44 ++++++++++++++++ Source/Android/jni/AndroidCommon/IDCache.h | 6 +++ .../jni/NetPlay/NetPlayUICallbacks.cpp | 52 +++++++++++++++++-- .../Android/jni/NetPlay/NetPlayUICallbacks.h | 11 +++- Source/Android/jni/NetPlay/Netplay.cpp | 17 +++++- 6 files changed, 131 insertions(+), 8 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 34cee1e5bfd6..5ede907e8587 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.services.GameFileCacheManager class NetplaySetupViewModel : ViewModel() { private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) @@ -26,6 +27,10 @@ class NetplaySetupViewModel : ViewModel() { private val _connectPort = MutableStateFlow(Netplay.getConnectPort().toString()) val connectPort = _connectPort.asStateFlow() + init { + GameFileCacheManager.startLoad() + } + fun setConnectionRole(connectionRole: ConnectionRole) { _connectionRole.value = connectionRole } @@ -55,6 +60,10 @@ class NetplaySetupViewModel : ViewModel() { } fun connect() { + if (GameFileCacheManager.isLoading().value == true) { + return + } + Netplay.saveSetup( nickname = nickname.value, connectionType = connectionType.value, diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 18b486023a98..1cd6e257e6cd 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -25,6 +25,13 @@ static jmethodID s_game_file_constructor; static jclass s_game_file_cache_class; static jfieldID s_game_file_cache_pointer; +static jclass s_game_file_cache_manager_class; +static jfieldID s_game_file_cache_manager_instance; + +static jclass s_netplay_class; +static jfieldID s_net_play_client_pointer; +static jmethodID s_netplay_on_msg_start_game; + static jclass s_analytics_class; static jmethodID s_get_analytics_value; @@ -220,6 +227,26 @@ jfieldID GetGameFileCachePointer() return s_game_file_cache_pointer; } +jclass GetGameFileCacheManagerClass() +{ + return s_game_file_cache_manager_class; +} + +jfieldID GetGameFileCacheManagerInstance() +{ + return s_game_file_cache_manager_instance; +} + +jclass GetNetplayClass() +{ + return s_netplay_class; +} + +jfieldID GetNetPlayClientPointer() +{ + return s_net_play_client_pointer; +} + jclass GetPairClass() { return s_pair_class; @@ -615,6 +642,21 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_game_file_cache_pointer = env->GetFieldID(game_file_cache_class, "pointer", "J"); env->DeleteLocalRef(game_file_cache_class); + const jclass game_file_cache_manager_class = + env->FindClass("org/dolphinemu/dolphinemu/services/GameFileCacheManager"); + s_game_file_cache_manager_class = + reinterpret_cast(env->NewGlobalRef(game_file_cache_manager_class)); + s_game_file_cache_manager_instance = env->GetStaticFieldID( + game_file_cache_manager_class, "gameFileCache", + "Lorg/dolphinemu/dolphinemu/model/GameFileCache;"); + env->DeleteLocalRef(game_file_cache_manager_class); + + const jclass netplay_class = + env->FindClass("org/dolphinemu/dolphinemu/features/netplay/Netplay"); + s_netplay_class = reinterpret_cast(env->NewGlobalRef(netplay_class)); + s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J"); + env->DeleteLocalRef(netplay_class); + const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics"); s_analytics_class = reinterpret_cast(env->NewGlobalRef(analytics_class)); s_get_analytics_value = env->GetStaticMethodID(s_analytics_class, "getValue", @@ -828,6 +870,8 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_game_file_class); env->DeleteGlobalRef(s_game_file_cache_class); + env->DeleteGlobalRef(s_game_file_cache_manager_class); + env->DeleteGlobalRef(s_netplay_class); env->DeleteGlobalRef(s_analytics_class); env->DeleteGlobalRef(s_pair_class); env->DeleteGlobalRef(s_hash_map_class); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0aaa9feec3fc..e13646eb5a93 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -28,6 +28,12 @@ jmethodID GetGameFileConstructor(); jclass GetGameFileCacheClass(); jfieldID GetGameFileCachePointer(); +jclass GetGameFileCacheManagerClass(); +jfieldID GetGameFileCacheManagerInstance(); + +jclass GetNetplayClass(); +jfieldID GetNetPlayClientPointer(); + jclass GetPairClass(); jmethodID GetPairConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index ab56458eff3b..31bc977e2ac0 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -3,9 +3,15 @@ #include "jni/NetPlay/NetPlayUICallbacks.h" +#include "jni/AndroidCommon/IDCache.h" + namespace NetPlay { -NetPlayUICallbacks::NetPlayUICallbacks() = default; +NetPlayUICallbacks::NetPlayUICallbacks(std::vector> games) + : m_games(std::move(games)) +{ +} + NetPlayUICallbacks::~NetPlayUICallbacks() = default; void NetPlayUICallbacks::BootGame(const std::string&, std::unique_ptr) {} @@ -13,9 +19,28 @@ void NetPlayUICallbacks::StopGame() {} bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() {} void NetPlayUICallbacks::AppendChat(const std::string&) {} -void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier&, const std::string&) {} + +void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, + const std::string& netplay_name) +{ + m_current_game_identifier = sync_identifier; + m_current_game_name = netplay_name; +} + void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} -void NetPlayUICallbacks::OnMsgStartGame() {} + +void NetPlayUICallbacks::OnMsgStartGame() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + auto* client = reinterpret_cast( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + if (client) + { + if (const auto game = FindGameFile(m_current_game_identifier)) + client->StartGame(game->GetFilePath()); + } +} + void NetPlayUICallbacks::OnMsgStopGame() {} void NetPlayUICallbacks::OnMsgPowerButton() {} void NetPlayUICallbacks::OnPlayerConnect(const std::string&) {} @@ -35,9 +60,26 @@ void NetPlayUICallbacks::OnIndexRefreshFailed(std::string) {} bool NetPlayUICallbacks::IsRecording() { return false; } std::shared_ptr -NetPlayUICallbacks::FindGameFile(const NetPlay::SyncIdentifier&, NetPlay::SyncIdentifierComparison*) +NetPlayUICallbacks::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, + NetPlay::SyncIdentifierComparison* found) { - return nullptr; + NetPlay::SyncIdentifierComparison temp; + if (!found) + found = &temp; + + *found = NetPlay::SyncIdentifierComparison::DifferentGame; + + std::shared_ptr result; + for (const auto& game : m_games) + { + const auto cmp = game->CompareSyncIdentifier(sync_identifier); + if (cmp < *found) + { + *found = cmp; + result = game; + } + } + return result; } std::string NetPlayUICallbacks::FindGBARomPath(const std::array&, std::string_view, int) { return {}; } diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index 18fdf1df93c3..acccfccabf48 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -1,14 +1,18 @@ #pragma once +#include #include +#include +#include #include "Core/NetPlayClient.h" +#include "UICommon/GameFile.h" namespace NetPlay { class NetPlayUICallbacks : public NetPlay::NetPlayUI { public: - NetPlayUICallbacks(); + NetPlayUICallbacks(std::vector> games); ~NetPlayUICallbacks() override; void BootGame(const std::string& filename, @@ -52,6 +56,11 @@ class NetPlayUICallbacks : public NetPlay::NetPlayUI { void HideChunkedProgressDialog() override; void SetChunkedProgress(int pid, u64 progress) override; void SetHostWiiSyncData(std::vector titles, std::string redirect_folder) override; + +private: + std::vector> m_games; + NetPlay::SyncIdentifier m_current_game_identifier; + std::string m_current_game_name; }; } // namespace NetPlay diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 42ed6483cebb..2346b685e069 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -3,14 +3,18 @@ #include #include +#include #include #include "Common/CommonTypes.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" +#include "UICommon/GameFile.h" +#include "UICommon/GameFileCache.h" #include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" #include "jni/NetPlay/NetPlayUICallbacks.h" extern "C" { @@ -121,7 +125,7 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup( } JNIEXPORT jlong JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv*, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) { const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); const bool is_traversal = traversal_choice == "traversal"; @@ -135,8 +139,17 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv*, jclass) const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); + jobject jgame_file_cache = env->GetStaticObjectField( + IDCache::GetGameFileCacheManagerClass(), IDCache::GetGameFileCacheManagerInstance()); + auto* game_file_cache = reinterpret_cast( + env->GetLongField(jgame_file_cache, IDCache::GetGameFileCachePointer())); + + std::vector> games; + game_file_cache->ForEach( + [&games](const std::shared_ptr& game) { games.push_back(game); }); + auto* client = new NetPlay::NetPlayClient( - host_ip, host_port, new NetPlay::NetPlayUICallbacks(), nickname, + host_ip, host_port, new NetPlay::NetPlayUICallbacks(std::move(games)), nickname, NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port}); return reinterpret_cast(client); From 3ed91742080194630bac072f4dbc9bdbffc94049 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 8 Apr 2026 15:20:19 +0100 Subject: [PATCH 039/270] Store netplay BootSessionData and use it to run the netplay game --- .../org/dolphinemu/dolphinemu/NativeLibrary.kt | 6 ++++++ .../dolphinemu/features/netplay/Netplay.kt | 17 +++++++++++++++++ .../features/netplay/ui/NetplaySetupActivity.kt | 9 +++++++++ .../dolphinemu/fragments/EmulationFragment.kt | 7 +++++++ Source/Android/jni/AndroidCommon/IDCache.cpp | 15 ++++++++++++++- Source/Android/jni/AndroidCommon/IDCache.h | 2 ++ Source/Android/jni/MainAndroid.cpp | 17 +++++++++++++++++ .../Android/jni/NetPlay/NetPlayUICallbacks.cpp | 13 ++++++++++--- 8 files changed, 82 insertions(+), 4 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt index 2ead4b534be8..2e2c295c7de8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -342,6 +342,12 @@ object NativeLibrary { @JvmStatic external fun RunSystemMenu() + /** + * Begins emulation for a netplay session, using the BootSessionData provided by the host. + */ + @JvmStatic + external fun RunNetPlay(paths: Array, riivolution: Boolean) + @JvmStatic external fun ChangeDisc(path: String) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index aaaca2b4fb22..6f8532a15c25 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -4,12 +4,23 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType object Netplay { @Keep private var netPlayClientPointer: Long = 0 + @Keep + private var bootSessionDataPointer: Long = 0 + + val isLaunching: Boolean + get() = bootSessionDataPointer != 0L + + private val _launchGame = Channel(Channel.CONFLATED) + val launchGame = _launchGame.receiveAsFlow() + @JvmStatic external fun getNickname(): String @@ -103,4 +114,10 @@ object Netplay { @JvmStatic private external fun Join(): Long + + @JvmStatic + fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { + this.bootSessionDataPointer = bootSessionDataPointer + _launchGame.trySend(gameFilePath) + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index 71d83ddf8191..ca1a8929017e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -46,7 +46,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.features.netplay.Netplay import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel @@ -64,6 +69,10 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { enableEdgeToEdge() super.onCreate(savedInstanceState) + Netplay.launchGame + .onEach { EmulationActivity.launch(this, it, false) } + .launchIn(lifecycleScope) + viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] setContent { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index e4698ba43c81..4ee4aee8aaa8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding +import org.dolphinemu.dolphinemu.features.netplay.Netplay import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.overlay.InputOverlay @@ -214,6 +215,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (launchSystemMenu) { Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") NativeLibrary.RunSystemMenu() + } else if (Netplay.isLaunching) { + Log.debug("[EmulationFragment] Starting emulation thread for Netplay.") + val paths = requireNotNull(gamePaths) { + "Cannot start emulation without any game paths" + } + NativeLibrary.RunNetPlay(paths, riivolution) } else { Log.debug("[EmulationFragment] Starting emulation thread.") val paths = requireNotNull(gamePaths) { diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 1cd6e257e6cd..9c41869f5be5 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -30,7 +30,8 @@ static jfieldID s_game_file_cache_manager_instance; static jclass s_netplay_class; static jfieldID s_net_play_client_pointer; -static jmethodID s_netplay_on_msg_start_game; +static jfieldID s_netplay_boot_session_data_pointer; +static jmethodID s_netplay_on_boot_game; static jclass s_analytics_class; static jmethodID s_get_analytics_value; @@ -247,6 +248,16 @@ jfieldID GetNetPlayClientPointer() return s_net_play_client_pointer; } +jfieldID GetNetplayBootSessionDataPointer() +{ + return s_netplay_boot_session_data_pointer; +} + +jmethodID GetNetplayOnBootGame() +{ + return s_netplay_on_boot_game; +} + jclass GetPairClass() { return s_pair_class; @@ -655,6 +666,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->FindClass("org/dolphinemu/dolphinemu/features/netplay/Netplay"); s_netplay_class = reinterpret_cast(env->NewGlobalRef(netplay_class)); s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J"); + s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); + s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); env->DeleteLocalRef(netplay_class); const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index e13646eb5a93..1879ff2afb34 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -33,6 +33,8 @@ jfieldID GetGameFileCacheManagerInstance(); jclass GetNetplayClass(); jfieldID GetNetPlayClientPointer(); +jfieldID GetNetplayBootSessionDataPointer(); +jmethodID GetNetplayOnBootGame(); jclass GetPairClass(); jmethodID GetPairConstructor(); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index a25d4a1d4a23..13471a6cd0fc 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -35,6 +35,7 @@ #include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" +#include "jni/NetPlay/NetPlayUICallbacks.h" #include "Core/BootManager.h" #include "Core/CommonTitles.h" #include "Core/ConfigLoaders/GameConfigLoader.h" @@ -616,6 +617,22 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_Run___3Ljava_lang_String_2ZLjava_la BootSessionData(GetJString(env, jSavestate), delete_state)); } +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunNetPlay( + JNIEnv* env, jclass, jobjectArray jPaths, jboolean jRiivolution) +{ + auto boot_session_data = std::unique_ptr(reinterpret_cast( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer()))); + if (!boot_session_data) + { + env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetDisplayToastMsg(), + ToJString(env, "Netplay: no boot session data"), JNI_TRUE); + env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetFinishEmulationActivity()); + return; + } + env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0); + Run(env, JStringArrayToVector(env, jPaths), jRiivolution, std::move(*boot_session_data)); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunSystemMenu(JNIEnv* env, jclass) { diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 31bc977e2ac0..27e7d36ee644 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -1,8 +1,10 @@ // Copyright 2003 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "jni/NetPlay/NetPlayUICallbacks.h" - +#include "UICommon/GameFile.h" +#include "NetPlayUICallbacks.h" +#include "Core/Boot/Boot.h" +#include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" namespace NetPlay { @@ -14,7 +16,12 @@ NetPlayUICallbacks::NetPlayUICallbacks(std::vector) {} +void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr boot_session_data) { + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnBootGame(), + ToJString(env, filename), reinterpret_cast(boot_session_data.release())); +} + void NetPlayUICallbacks::StopGame() {} bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() {} From f52ca02ca24f06697564441730f86a03a8f3e7fe Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Thu, 9 Apr 2026 12:40:07 +0100 Subject: [PATCH 040/270] Show client connection errors and handle connection result If result is a success sent event to launch the next netplay screen. if it fails, clear up the netplay client --- .../dolphinemu/features/netplay/Netplay.kt | 49 ++++++++++++++- .../features/netplay/model/ConnectionRole.kt | 12 +++- .../netplay/model/NetplaySetupViewModel.kt | 44 +++++++++---- .../netplay/ui/NetplaySetupActivity.kt | 61 ++++++++++++++++++- .../app/src/main/res/values/strings.xml | 4 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 7 +++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 9 ++- Source/Android/jni/NetPlay/Netplay.cpp | 18 ++++++ 9 files changed, 184 insertions(+), 21 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 6f8532a15c25..fcad9ddb3fa0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -4,8 +4,11 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType object Netplay { @@ -21,6 +24,12 @@ object Netplay { private val _launchGame = Channel(Channel.CONFLATED) val launchGame = _launchGame.receiveAsFlow() + private val _connectionErrors = Channel(Channel.BUFFERED) + val connectionErrors = _connectionErrors.receiveAsFlow() + + @JvmStatic + external fun isClientConnected(): Boolean + @JvmStatic external fun getNickname(): String @@ -63,13 +72,13 @@ object Netplay { @JvmStatic external fun getIndexPassword(): String - fun saveSetup( + suspend fun saveSetup( nickname: String, connectionType: ConnectionType, address: String, hostCode: String, connectPort: Int, - ) { + ) = withContext(Dispatchers.IO) { SaveSetup( nickname = nickname, traversalChoice = connectionType.configValue, @@ -108,16 +117,50 @@ object Netplay { indexPassword: String, ) - fun join() { + suspend fun join(): Boolean = withContext(Dispatchers.IO) { netPlayClientPointer = Join() + val isConnected = netPlayClientPointer != 0L && isClientConnected() + + if (!isActive) { + releaseNetplayClient() + return@withContext false + } + + if (isConnected) { + return@withContext true + } + + releaseNetplayClient() + false + } + + private fun releaseNetplayClient() { + if (netPlayClientPointer != 0L) { + ReleaseNetplayClient() + netPlayClientPointer = 0 + } + _launchGame.flush() + _connectionErrors.flush() } @JvmStatic private external fun Join(): Long + @JvmStatic + private external fun ReleaseNetplayClient() + @JvmStatic fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { this.bootSessionDataPointer = bootSessionDataPointer _launchGame.trySend(gameFilePath) } + + @JvmStatic + fun onConnectionError(message: String) { + _connectionErrors.trySend(message) + } +} + +private fun Channel.flush() { + while (this.tryReceive().isSuccess) Unit } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt index a54d08256607..a9f86d5982c9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt @@ -5,9 +5,17 @@ import org.dolphinemu.dolphinemu.R sealed class ConnectionRole( @StringRes val labelId: Int, + @StringRes val loadingLabelId: Int, ) { - object Connect : ConnectionRole(R.string.netplay_connection_role_connect) - object Host : ConnectionRole(R.string.netplay_connection_role_host) + object Connect : ConnectionRole( + labelId = R.string.netplay_connection_role_connect, + loadingLabelId = R.string.netplay_connection_role_connect_loading, + ) + + object Host : ConnectionRole( + labelId = R.string.netplay_connection_role_host, + loadingLabelId = R.string.netplay_connection_role_host_loading, + ) companion object { val all: List diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 5ede907e8587..19175e4df85b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -3,8 +3,15 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay import org.dolphinemu.dolphinemu.services.GameFileCacheManager @@ -27,6 +34,14 @@ class NetplaySetupViewModel : ViewModel() { private val _connectPort = MutableStateFlow(Netplay.getConnectPort().toString()) val connectPort = _connectPort.asStateFlow() + private val _showNetplayScreen = Channel(CONFLATED) + val showNetplayScreen = _showNetplayScreen.receiveAsFlow() + + private val _connecting = MutableStateFlow(false) + val connecting = _connecting.asStateFlow() + + val errors = Netplay.connectionErrors + init { GameFileCacheManager.startLoad() } @@ -60,17 +75,24 @@ class NetplaySetupViewModel : ViewModel() { } fun connect() { - if (GameFileCacheManager.isLoading().value == true) { - return - } + _connecting.value = true - Netplay.saveSetup( - nickname = nickname.value, - connectionType = connectionType.value, - address = ipAddress.value, - hostCode = hostCode.value, - connectPort = connectPort.value.toInt(), - ) - Netplay.join() + viewModelScope.launch { + GameFileCacheManager.isLoading().asFlow().first { it == false } + + Netplay.saveSetup( + nickname = nickname.value, + connectionType = connectionType.value, + address = ipAddress.value, + hostCode = hostCode.value, + connectPort = connectPort.value.toInt(), + ) + + if (Netplay.join()) { + _showNetplayScreen.trySend(Unit) + } + + _connecting.value = false + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index ca1a8929017e..5032546cb6cd 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -11,19 +11,24 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -35,7 +40,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -45,8 +52,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.dolphinemu.dolphinemu.R @@ -60,6 +71,10 @@ import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.utils.ThemeHelper +private data class ErrorDialogState(val message: String) { + val onDismissed = CompletableDeferred() +} + class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { override var themeId: Int = 0 private lateinit var viewModel: NetplaySetupViewModel @@ -75,10 +90,16 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] + viewModel.showNetplayScreen + .onEach { /* launch NetplayActivity */ } + .launchIn(lifecycleScope) + setContent { DolphinTheme { NetplaySetupScreen( onBackClicked = { finish() }, + connecting = viewModel.connecting.collectAsState().value, + errors = viewModel.errors, nickname = viewModel.nickname.collectAsState().value, onNicknameChanged = viewModel::setNickname, connectionType = viewModel.connectionType.collectAsState().value, @@ -118,6 +139,8 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { @Composable private fun NetplaySetupScreen( onBackClicked: () -> Unit, + connecting: Boolean, + errors: Flow, connectionRole: ConnectionRole, onConnectionRoleChanged: (ConnectionRole) -> Unit, nickname: String, @@ -150,10 +173,40 @@ private fun NetplaySetupScreen( ExtendedFloatingActionButton( onClick = onConnectClicked, ) { - Text(stringResource(connectionRole.labelId)) + if (connecting) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Spacer(Modifier.width(12.dp)) + Text(stringResource(connectionRole.loadingLabelId)) + } else { + Text(stringResource(connectionRole.labelId)) + } } } ) { innerPadding -> + var activeErrorDialog by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + errors.collect { message -> + activeErrorDialog = ErrorDialogState(message) + activeErrorDialog?.onDismissed?.await() + activeErrorDialog = null + } + } + activeErrorDialog?.let { activeErrorDialog -> + AlertDialog( + text = { Text(activeErrorDialog.message) }, + confirmButton = { + TextButton(onClick = { activeErrorDialog.onDismissed.complete(Unit) }) { + Text("Dismiss") + } + }, + onDismissRequest = { activeErrorDialog.onDismissed.complete(Unit) }, + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -332,12 +385,14 @@ private fun NetplaySetupScreenPreview() { MaterialTheme { NetplaySetupScreen( onBackClicked = {}, + connecting = false, + errors = emptyFlow(), + connectionRole = ConnectionRole.Connect, + onConnectionRoleChanged = {}, nickname = "Preview nickname", onNicknameChanged = {}, connectionType = ConnectionType.DirectConnection, onConnectionTypeChanged = {}, - connectionRole = ConnectionRole.Connect, - onConnectionRoleChanged = {}, ipAddress = "127.0.0.1", onIpAddressChanged = {}, connectPort = "2626", diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 78c9c3b34cb5..1944cc4911df 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -984,12 +984,14 @@ It can efficiently compress both junk data and encrypted Wii data. Netplay Setup - Nickname Connection type Direct connection Traversal server Connect + Connecting… Host + Starting… + Nickname IP address Host code Port diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 9c41869f5be5..8f74425efac5 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -32,6 +32,7 @@ static jclass s_netplay_class; static jfieldID s_net_play_client_pointer; static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; +static jmethodID s_netplay_on_connection_error; static jclass s_analytics_class; static jmethodID s_get_analytics_value; @@ -258,6 +259,11 @@ jmethodID GetNetplayOnBootGame() return s_netplay_on_boot_game; } +jmethodID GetNetplayOnConnectionError() +{ + return s_netplay_on_connection_error; +} + jclass GetPairClass() { return s_pair_class; @@ -668,6 +674,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J"); s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); + s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); env->DeleteLocalRef(netplay_class); const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 1879ff2afb34..d59d1ca9f08a 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -35,6 +35,7 @@ jclass GetNetplayClass(); jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); +jmethodID GetNetplayOnConnectionError(); jclass GetPairClass(); jmethodID GetPairConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 27e7d36ee644..baa38de18d2d 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -56,7 +56,14 @@ void NetPlayUICallbacks::OnPadBufferChanged(u32) {} void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool) {} void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} void NetPlayUICallbacks::OnConnectionLost() {} -void NetPlayUICallbacks::OnConnectionError(const std::string&) {} + +void NetPlayUICallbacks::OnConnectionError(const std::string& message) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionError(), + ToJString(env, message)); +} + void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {} void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State) {} void NetPlayUICallbacks::OnGameStartAborted() {} diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 2346b685e069..c5fdd21ad5ee 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -17,6 +17,12 @@ #include "jni/AndroidCommon/IDCache.h" #include "jni/NetPlay/NetPlayUICallbacks.h" +static NetPlay::NetPlayClient* GetPointer(JNIEnv* env) +{ + return reinterpret_cast( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); +} + extern "C" { JNIEXPORT jstring JNICALL @@ -124,6 +130,12 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup( Config::SetBaseOrCurrent(Config::NETPLAY_LISTEN_PORT, static_cast(listenPort)); } +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass) +{ + return static_cast(GetPointer(env)->IsConnected()); +} + JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) { @@ -155,4 +167,10 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass return reinterpret_cast(client); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseNetplayClient(JNIEnv* env, jclass) +{ + delete GetPointer(env); +} + } // extern "C" From 5527e8138e90e03e2d5cd4fc3db78e3c7f0b523c Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 15 Apr 2026 18:40:24 +0200 Subject: [PATCH 041/270] Move NetplaySetupScreen to its own file --- .../netplay/ui/NetplaySetupActivity.kt | 326 +---------------- .../features/netplay/ui/NetplaySetupScreen.kt | 330 ++++++++++++++++++ 2 files changed, 331 insertions(+), 325 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index 5032546cb6cd..8e9cf05422b2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -@file:OptIn(ExperimentalMaterial3Api::class) - package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.Context @@ -10,74 +8,20 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar -import androidx.compose.material3.MenuAnchorType -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SecondaryTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.features.netplay.Netplay -import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole -import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme -import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.utils.ThemeHelper -private data class ErrorDialogState(val message: String) { - val onDismissed = CompletableDeferred() -} - class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { override var themeId: Int = 0 - private lateinit var viewModel: NetplaySetupViewModel override fun onCreate(savedInstanceState: Bundle?) { ThemeHelper.setTheme(this) @@ -88,7 +32,7 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { .onEach { EmulationActivity.launch(this, it, false) } .launchIn(lifecycleScope) - viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] + val viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] viewModel.showNetplayScreen .onEach { /* launch NetplayActivity */ } @@ -135,271 +79,3 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { } } } - -@Composable -private fun NetplaySetupScreen( - onBackClicked: () -> Unit, - connecting: Boolean, - errors: Flow, - connectionRole: ConnectionRole, - onConnectionRoleChanged: (ConnectionRole) -> Unit, - nickname: String, - onNicknameChanged: (String) -> Unit, - connectionType: ConnectionType, - onConnectionTypeChanged: (ConnectionType) -> Unit, - ipAddress: String, - onIpAddressChanged: (String) -> Unit, - connectPort: String, - onConnectPortChanged: (String) -> Unit, - hostCode: String, - onHostCodeChanged: (String) -> Unit, - onConnectClicked: () -> Unit, -) { - Scaffold( - topBar = { - MediumTopAppBar( - title = { Text(stringResource(R.string.netplay_setup_title)) }, - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - ) - }, - floatingActionButton = { - ExtendedFloatingActionButton( - onClick = onConnectClicked, - ) { - if (connecting) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - Spacer(Modifier.width(12.dp)) - Text(stringResource(connectionRole.loadingLabelId)) - } else { - Text(stringResource(connectionRole.labelId)) - } - } - } - ) { innerPadding -> - var activeErrorDialog by remember { mutableStateOf(null) } - LaunchedEffect(Unit) { - errors.collect { message -> - activeErrorDialog = ErrorDialogState(message) - activeErrorDialog?.onDismissed?.await() - activeErrorDialog = null - } - } - activeErrorDialog?.let { activeErrorDialog -> - AlertDialog( - text = { Text(activeErrorDialog.message) }, - confirmButton = { - TextButton(onClick = { activeErrorDialog.onDismissed.complete(Unit) }) { - Text("Dismiss") - } - }, - onDismissRequest = { activeErrorDialog.onDismissed.complete(Unit) }, - ) - } - - Column( - modifier = Modifier - .fillMaxSize() - .consumeWindowInsets(innerPadding) - .verticalScroll(rememberScrollState()) - .padding(innerPadding) - ) { - SecondaryTabRow(selectedTabIndex = ConnectionRole.all.indexOf(connectionRole)) { - ConnectionRole.all.forEach { role -> - Tab( - selected = connectionRole == role, - onClick = { onConnectionRoleChanged(role) }, - text = { Text(stringResource(role.labelId)) }, - ) - } - } - - MenuSpacer() - - Column( - modifier = Modifier - .padding(horizontal = DolphinTheme.scaffoldPadding) - ) { - NetplaySetupContent( - nickname = nickname, - onNicknameChanged = onNicknameChanged, - connectionType = connectionType, - onConnectionTypeChanged = onConnectionTypeChanged, - ipAddress = ipAddress, - onIpAddressChanged = onIpAddressChanged, - hostCode = hostCode, - onHostCodeChanged = onHostCodeChanged, - connectPort = connectPort, - onConnectPortChanged = onConnectPortChanged, - connectionRole = connectionRole, - ) - } - } - } -} - -@Composable -private fun NetplaySetupContent( - nickname: String, - onNicknameChanged: (String) -> Unit, - connectionType: ConnectionType, - onConnectionTypeChanged: (ConnectionType) -> Unit, - ipAddress: String, - onIpAddressChanged: (String) -> Unit, - hostCode: String, - onHostCodeChanged: (String) -> Unit, - connectPort: String, - onConnectPortChanged: (String) -> Unit, - connectionRole: ConnectionRole, -) { - OutlinedTextField( - value = nickname, - onValueChange = onNicknameChanged, - label = { Text(stringResource(R.string.netplay_nickname_label)) }, - modifier = Modifier.fillMaxWidth() - ) - - MenuSpacer() - - ConnectionTypePicker( - connectionType = connectionType, - onConnectionTypeChanged = onConnectionTypeChanged, - ) - - MenuSpacer() - - when (connectionRole) { - ConnectionRole.Connect -> ConnectMenu( - connectionType = connectionType, - ipAddress = ipAddress, - onIpAddressChanged = onIpAddressChanged, - hostCode = hostCode, - onHostCodeChanged = onHostCodeChanged, - port = connectPort, - onPortChanged = onConnectPortChanged, - ) - - ConnectionRole.Host -> {} - } -} - -@Composable -private fun ConnectionTypePicker( - connectionType: ConnectionType, - onConnectionTypeChanged: (ConnectionType) -> Unit, -) { - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - ConnectionType.all.forEach { connectionType -> - DropdownMenuItem( - text = { Text(stringResource(connectionType.labelId)) }, - onClick = { - onConnectionTypeChanged(connectionType) - expanded = false - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, - ) - } - } - OutlinedTextField( - value = stringResource(connectionType.labelId), - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(R.string.netplay_connection_type)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, - modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .fillMaxWidth() - ) - } -} - -@Composable -fun ConnectMenu( - connectionType: ConnectionType, - ipAddress: String, - onIpAddressChanged: (String) -> Unit, - hostCode: String, - onHostCodeChanged: (String) -> Unit, - port: String, - onPortChanged: (String) -> Unit, -) { - when (connectionType) { - ConnectionType.DirectConnection -> { - OutlinedTextField( - value = ipAddress, - onValueChange = onIpAddressChanged, - label = { Text(stringResource(R.string.netplay_ip_address_label)) }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - ), - modifier = Modifier - .fillMaxWidth() - ) - - MenuSpacer() - - OutlinedTextField( - value = port, - onValueChange = onPortChanged, - label = { Text(stringResource(R.string.netplay_port_label)) }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - ), - modifier = Modifier - .fillMaxWidth() - ) - } - - ConnectionType.TraversalServer -> OutlinedTextField( - value = hostCode, - onValueChange = onHostCodeChanged, - label = { Text(stringResource(R.string.netplay_host_code_label)) }, - modifier = Modifier - .fillMaxWidth() - ) - } -} - -@Preview -@Composable -private fun NetplaySetupScreenPreview() { - MaterialTheme { - NetplaySetupScreen( - onBackClicked = {}, - connecting = false, - errors = emptyFlow(), - connectionRole = ConnectionRole.Connect, - onConnectionRoleChanged = {}, - nickname = "Preview nickname", - onNicknameChanged = {}, - connectionType = ConnectionType.DirectConnection, - onConnectionTypeChanged = {}, - ipAddress = "127.0.0.1", - onIpAddressChanged = {}, - connectPort = "2626", - onConnectPortChanged = {}, - hostCode = "", - onHostCodeChanged = {}, - onConnectClicked = {}, - ) - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt new file mode 100644 index 000000000000..bcc11ef4bb12 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -0,0 +1,330 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package org.dolphinemu.dolphinemu.features.netplay.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SecondaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole +import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType +import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme +import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer + +private data class ErrorDialogState(val message: String) { + val onDismissed = CompletableDeferred() +} + +@Composable +fun NetplaySetupScreen( + onBackClicked: () -> Unit, + connecting: Boolean, + errors: Flow, + connectionRole: ConnectionRole, + onConnectionRoleChanged: (ConnectionRole) -> Unit, + nickname: String, + onNicknameChanged: (String) -> Unit, + connectionType: ConnectionType, + onConnectionTypeChanged: (ConnectionType) -> Unit, + ipAddress: String, + onIpAddressChanged: (String) -> Unit, + connectPort: String, + onConnectPortChanged: (String) -> Unit, + hostCode: String, + onHostCodeChanged: (String) -> Unit, + onConnectClicked: () -> Unit, +) { + Scaffold( + topBar = { + MediumTopAppBar( + title = { Text(stringResource(R.string.netplay_setup_title)) }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = onConnectClicked, + ) { + if (connecting) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Spacer(Modifier.width(12.dp)) + Text(stringResource(connectionRole.loadingLabelId)) + } else { + Text(stringResource(connectionRole.labelId)) + } + } + } + ) { innerPadding -> + var activeErrorDialog by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + errors.collect { message -> + activeErrorDialog = ErrorDialogState(message) + activeErrorDialog?.onDismissed?.await() + activeErrorDialog = null + } + } + activeErrorDialog?.let { activeErrorDialog -> + AlertDialog( + text = { Text(activeErrorDialog.message) }, + confirmButton = { + TextButton(onClick = { activeErrorDialog.onDismissed.complete(Unit) }) { + Text("Dismiss") + } + }, + onDismissRequest = { activeErrorDialog.onDismissed.complete(Unit) }, + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .consumeWindowInsets(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(innerPadding) + ) { + SecondaryTabRow(selectedTabIndex = ConnectionRole.all.indexOf(connectionRole)) { + ConnectionRole.all.forEach { role -> + Tab( + selected = connectionRole == role, + onClick = { onConnectionRoleChanged(role) }, + text = { Text(stringResource(role.labelId)) }, + ) + } + } + + MenuSpacer() + + Column( + modifier = Modifier + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) { + NetplaySetupContent( + nickname = nickname, + onNicknameChanged = onNicknameChanged, + connectionType = connectionType, + onConnectionTypeChanged = onConnectionTypeChanged, + ipAddress = ipAddress, + onIpAddressChanged = onIpAddressChanged, + hostCode = hostCode, + onHostCodeChanged = onHostCodeChanged, + connectPort = connectPort, + onConnectPortChanged = onConnectPortChanged, + connectionRole = connectionRole, + ) + } + } + } +} + +@Composable +private fun NetplaySetupContent( + nickname: String, + onNicknameChanged: (String) -> Unit, + connectionType: ConnectionType, + onConnectionTypeChanged: (ConnectionType) -> Unit, + ipAddress: String, + onIpAddressChanged: (String) -> Unit, + hostCode: String, + onHostCodeChanged: (String) -> Unit, + connectPort: String, + onConnectPortChanged: (String) -> Unit, + connectionRole: ConnectionRole, +) { + OutlinedTextField( + value = nickname, + onValueChange = onNicknameChanged, + label = { Text(stringResource(R.string.netplay_nickname_label)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + MenuSpacer() + + ConnectionTypePicker( + connectionType = connectionType, + onConnectionTypeChanged = onConnectionTypeChanged, + ) + + MenuSpacer() + + when (connectionRole) { + ConnectionRole.Connect -> ConnectMenu( + connectionType = connectionType, + ipAddress = ipAddress, + onIpAddressChanged = onIpAddressChanged, + hostCode = hostCode, + onHostCodeChanged = onHostCodeChanged, + port = connectPort, + onPortChanged = onConnectPortChanged, + ) + + ConnectionRole.Host -> {} + } +} + +@Composable +private fun ConnectionTypePicker( + connectionType: ConnectionType, + onConnectionTypeChanged: (ConnectionType) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + ConnectionType.all.forEach { connectionType -> + DropdownMenuItem( + text = { Text(stringResource(connectionType.labelId)) }, + onClick = { + onConnectionTypeChanged(connectionType) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + OutlinedTextField( + value = stringResource(connectionType.labelId), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.netplay_connection_type)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth() + ) + } +} + +@Composable +fun ConnectMenu( + connectionType: ConnectionType, + ipAddress: String, + onIpAddressChanged: (String) -> Unit, + hostCode: String, + onHostCodeChanged: (String) -> Unit, + port: String, + onPortChanged: (String) -> Unit, +) { + when (connectionType) { + ConnectionType.DirectConnection -> { + OutlinedTextField( + value = ipAddress, + onValueChange = onIpAddressChanged, + label = { Text(stringResource(R.string.netplay_ip_address_label)) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + ) + + MenuSpacer() + + OutlinedTextField( + value = port, + onValueChange = onPortChanged, + label = { Text(stringResource(R.string.netplay_port_label)) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + ) + } + + ConnectionType.TraversalServer -> OutlinedTextField( + value = hostCode, + onValueChange = onHostCodeChanged, + label = { Text(stringResource(R.string.netplay_host_code_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + ) + } +} + +@Preview +@Composable +private fun NetplaySetupScreenPreview() { + MaterialTheme { + NetplaySetupScreen( + onBackClicked = {}, + connecting = false, + errors = emptyFlow(), + connectionRole = ConnectionRole.Connect, + onConnectionRoleChanged = {}, + nickname = "Preview nickname", + onNicknameChanged = {}, + connectionType = ConnectionType.DirectConnection, + onConnectionTypeChanged = {}, + ipAddress = "127.0.0.1", + onIpAddressChanged = {}, + connectPort = "2626", + onConnectPortChanged = {}, + hostCode = "", + onHostCodeChanged = {}, + onConnectClicked = {}, + ) + } +} From b72f3c1afc6a4d4ba3dfffa19de33a425c55d679 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Thu, 9 Apr 2026 13:43:57 +0100 Subject: [PATCH 042/270] Add mostly empty Netplay screen, equivalent of NetPlayDialog in QT. All it can do at this point is quit the current netplay session when backing out of this screen. --- .../Android/app/src/main/AndroidManifest.xml | 5 ++ .../dolphinemu/features/netplay/Netplay.kt | 4 ++ .../netplay/model/NetplayViewModel.kt | 33 +++++++++ .../features/netplay/ui/NetplayActivity.kt | 68 +++++++++++++++++++ .../features/netplay/ui/NetplayScreen.kt | 46 +++++++++++++ .../netplay/ui/NetplaySetupActivity.kt | 11 ++- .../app/src/main/res/values/strings.xml | 1 + 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index e68fa9f7578f..96876acd46c5 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -138,6 +138,11 @@ android:exported="false" android:theme="@style/Theme.Dolphin.Main" /> + + (CONFLATED) + val goBack = _goBack.receiveAsFlow() + + init { + if (!Netplay.isClientConnected()) { + _goBack.trySend(Unit) + } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onCleared() { + super.onCleared() + GlobalScope.launch { + Netplay.quit() + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt new file mode 100644 index 000000000000..7f199bd45e0c --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel +import org.dolphinemu.dolphinemu.ui.main.ThemeProvider +import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme +import org.dolphinemu.dolphinemu.utils.ThemeHelper + +class NetplayActivity : AppCompatActivity(), ThemeProvider { + override var themeId: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.setTheme(this) + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + val viewModel = ViewModelProvider(this)[NetplayViewModel::class.java] + + viewModel.goBack + .onEach { finish() } + .launchIn(lifecycleScope) + + viewModel.launchGame + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .onEach { EmulationActivity.launch(this, it, false) } + .launchIn(lifecycleScope) + + setContent { + DolphinTheme { + NetplayScreen( + onBackClicked = { finish() }, + ) + } + } + } + + override fun setTheme(themeId: Int) { + super.setTheme(themeId) + this.themeId = themeId + } + + override fun onResume() { + ThemeHelper.setCorrectTheme(this) + super.onResume() + } + + companion object { + @JvmStatic + fun launch(context: Context) { + context.startActivity(Intent(context, NetplayActivity::class.java)) + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt new file mode 100644 index 000000000000..de3b89d6c1a5 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import org.dolphinemu.dolphinemu.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NetplayScreen( + onBackClicked: () -> Unit, +) { + Scaffold( + topBar = { + MediumTopAppBar( + title = { Text(stringResource(R.string.netplay_title)) }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + ) + }, + ) { _ -> } +} + +@Preview +@Composable +private fun NetplayScreenPreview() { + NetplayScreen( + onBackClicked = {}, + ) +} \ No newline at end of file diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index 8e9cf05422b2..8058b3ad96e8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -9,12 +9,12 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.collectAsState +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.dolphinemu.dolphinemu.activities.EmulationActivity -import org.dolphinemu.dolphinemu.features.netplay.Netplay import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @@ -28,14 +28,11 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { enableEdgeToEdge() super.onCreate(savedInstanceState) - Netplay.launchGame - .onEach { EmulationActivity.launch(this, it, false) } - .launchIn(lifecycleScope) - val viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] viewModel.showNetplayScreen - .onEach { /* launch NetplayActivity */ } + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .onEach { NetplayActivity.launch(this) } .launchIn(lifecycleScope) setContent { diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 1944cc4911df..903a414af380 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -995,4 +995,5 @@ It can efficiently compress both junk data and encrypted Wii data. IP address Host code Port + Netplay From af2fda564954b975115b212221aa13f0308fe406 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Thu, 9 Apr 2026 23:26:58 +0200 Subject: [PATCH 043/270] Reorder netplay class Put all the boring settings at the bottom to reduce scrolling! --- .../dolphinemu/features/netplay/Netplay.kt | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 41b2bb6f8a80..9b19c9c2fde7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -27,9 +27,60 @@ object Netplay { private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { + netPlayClientPointer = Join() + val isConnected = netPlayClientPointer != 0L && isClientConnected() + + if (!isActive) { + releaseNetplayClient() + return@withContext false + } + + if (isConnected) { + return@withContext true + } + + releaseNetplayClient() + false + } + + suspend fun quit() = withContext(Dispatchers.IO) { + releaseNetplayClient() + } + + private fun releaseNetplayClient() { + if (netPlayClientPointer != 0L) { + ReleaseNetplayClient() + netPlayClientPointer = 0 + } + _launchGame.flush() + _connectionErrors.flush() + } + + @JvmStatic + private external fun Join(): Long + @JvmStatic external fun isClientConnected(): Boolean + @JvmStatic + private external fun ReleaseNetplayClient() + + // NetPlayUI callbacks + + @JvmStatic + fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { + this.bootSessionDataPointer = bootSessionDataPointer + _launchGame.trySend(gameFilePath) + } + + @JvmStatic + fun onConnectionError(message: String) { + _connectionErrors.trySend(message) + } + + // Settings + @JvmStatic external fun getNickname(): String @@ -116,53 +167,6 @@ object Netplay { indexName: String, indexPassword: String, ) - - suspend fun join(): Boolean = withContext(Dispatchers.IO) { - netPlayClientPointer = Join() - val isConnected = netPlayClientPointer != 0L && isClientConnected() - - if (!isActive) { - releaseNetplayClient() - return@withContext false - } - - if (isConnected) { - return@withContext true - } - - releaseNetplayClient() - false - } - - suspend fun quit() = withContext(Dispatchers.IO) { - releaseNetplayClient() - } - - private fun releaseNetplayClient() { - if (netPlayClientPointer != 0L) { - ReleaseNetplayClient() - netPlayClientPointer = 0 - } - _launchGame.flush() - _connectionErrors.flush() - } - - @JvmStatic - private external fun Join(): Long - - @JvmStatic - private external fun ReleaseNetplayClient() - - @JvmStatic - fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { - this.bootSessionDataPointer = bootSessionDataPointer - _launchGame.trySend(gameFilePath) - } - - @JvmStatic - fun onConnectionError(message: String) { - _connectionErrors.trySend(message) - } } private fun Channel.flush() { From d9e2725e85eafecd9dac67dfdec8a3825d6933e1 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Fri, 10 Apr 2026 11:30:59 +0200 Subject: [PATCH 044/270] Implement player list --- .../dolphinemu/features/netplay/Netplay.kt | 19 +++ .../netplay/model/NetplayViewModel.kt | 6 + .../features/netplay/model/Player.kt | 13 ++ .../features/netplay/ui/NetplayActivity.kt | 3 +- .../features/netplay/ui/NetplayScreen.kt | 119 +++++++++++++++++- Source/Android/jni/AndroidCommon/IDCache.cpp | 27 ++++ Source/Android/jni/AndroidCommon/IDCache.h | 4 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 36 +++++- 8 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 9b19c9c2fde7..ceda99505107 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -5,11 +5,17 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType +import org.dolphinemu.dolphinemu.features.netplay.model.Player object Netplay { @Keep @@ -27,6 +33,12 @@ object Netplay { private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() + private val _players = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val players = _players.asSharedFlow().distinctUntilChanged() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { netPlayClientPointer = Join() val isConnected = netPlayClientPointer != 0L && isClientConnected() @@ -48,6 +60,7 @@ object Netplay { releaseNetplayClient() } + @OptIn(ExperimentalCoroutinesApi::class) private fun releaseNetplayClient() { if (netPlayClientPointer != 0L) { ReleaseNetplayClient() @@ -55,6 +68,7 @@ object Netplay { } _launchGame.flush() _connectionErrors.flush() + _players.resetReplayCache() } @JvmStatic @@ -79,6 +93,11 @@ object Netplay { _connectionErrors.trySend(message) } + @JvmStatic + fun onUpdate(players: Array) { + _players.tryEmit(players.toList()) + } + // Settings @JvmStatic diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index cf941aa01f81..942dc0d0dc1c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -3,11 +3,14 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay @@ -17,6 +20,9 @@ class NetplayViewModel : ViewModel() { private val _goBack = Channel(CONFLATED) val goBack = _goBack.receiveAsFlow() + val players = Netplay.players + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + init { if (!Netplay.isClientConnected()) { _goBack.trySend(Unit) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt new file mode 100644 index 000000000000..70c228d1c906 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt @@ -0,0 +1,13 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay.model + +data class Player( + val pid: Int, + val name: String, + val revision: String, + val ping: Int, + val isHost: Boolean, + val mapping: String, +) \ No newline at end of file diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index 7f199bd45e0c..5a91ae5b91a5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -8,6 +8,7 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.collectAsState import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle @@ -15,7 +16,6 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.dolphinemu.dolphinemu.activities.EmulationActivity -import org.dolphinemu.dolphinemu.features.netplay.Netplay import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @@ -44,6 +44,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { DolphinTheme { NetplayScreen( onBackClicked = { finish() }, + players = viewModel.players.collectAsState().value, ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index de3b89d6c1a5..e4089dae4546 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -2,23 +2,46 @@ package org.dolphinemu.dolphinemu.features.netplay.ui +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.netplay.model.Player +import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun NetplayScreen( onBackClicked: () -> Unit, + players: List, ) { Scaffold( topBar = { @@ -34,7 +57,81 @@ fun NetplayScreen( }, ) }, - ) { _ -> } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .consumeWindowInsets(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(innerPadding) + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) { + PlayersTable( + rows = buildList { + add(listOf("Player", "Ping", "Mapping")) + addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) }) + }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +/** + * A table arranged into columns sized to wrap the largest item. Except the + * first column which takes up the remaining space left by the other columns. + * The first row is treated as the column titles. + */ +@Composable +private fun PlayersTable( + rows: List>, + modifier: Modifier = Modifier, +) { + rows.zipWithNext { a, b -> if (a.size != b.size) throw IllegalArgumentException("Rows must all contain the same number of elements.") } + val maxWidths = remember { List(rows.first().size) { mutableIntStateOf(0) } } + val density = LocalDensity.current + + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = modifier + ) { + rows.forEachIndexed { rowIndex, row -> + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + row.forEachIndexed { itemIndex, text -> + Box( + modifier = Modifier + .then( + when { + itemIndex == 0 -> Modifier.weight(1f) + + maxWidths[itemIndex].intValue > 0 -> Modifier + .width(with(density) { maxWidths[itemIndex].intValue.toDp() }) + + else -> Modifier + } + ) + .onGloballyPositioned { coordinates -> + val width = coordinates.size.width + if (width > maxWidths[itemIndex].intValue) { + maxWidths[itemIndex].intValue = width + } + } + ) { + Text( + text = text, + fontWeight = if (rowIndex == 0) FontWeight.Medium else FontWeight.Normal, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + if (rowIndex == 0) { + HorizontalDivider() + } + } + } } @Preview @@ -42,5 +139,23 @@ fun NetplayScreen( private fun NetplayScreenPreview() { NetplayScreen( onBackClicked = {}, + players = listOf( + Player( + pid = 1, + name = "Player 1", + revision = "123", + ping = 2, + isHost = true, + mapping = "m1" + ), + Player( + pid = 2, + name = "Player 2", + revision = "123", + ping = 23, + isHost = false, + mapping = "m2" + ), + ), ) -} \ No newline at end of file +} diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 8f74425efac5..b45849bf5dc8 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -33,6 +33,10 @@ static jfieldID s_net_play_client_pointer; static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_connection_error; +static jmethodID s_netplay_update; + +static jclass s_netplay_player_class; +static jmethodID s_netplay_player_constructor; static jclass s_analytics_class; static jmethodID s_get_analytics_value; @@ -264,6 +268,21 @@ jmethodID GetNetplayOnConnectionError() return s_netplay_on_connection_error; } +jmethodID GetNetplayUpdate() +{ + return s_netplay_update; +} + +jclass GetNetplayPlayerClass() +{ + return s_netplay_player_class; +} + +jmethodID GetNetplayPlayerConstructor() +{ + return s_netplay_player_constructor; +} + jclass GetPairClass() { return s_pair_class; @@ -675,8 +694,15 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); + s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); env->DeleteLocalRef(netplay_class); + const jclass netplay_player_class = + env->FindClass("org/dolphinemu/dolphinemu/features/netplay/model/Player"); + s_netplay_player_class = reinterpret_cast(env->NewGlobalRef(netplay_player_class)); + s_netplay_player_constructor = env->GetMethodID(netplay_player_class, "", "(ILjava/lang/String;Ljava/lang/String;IZLjava/lang/String;)V"); + env->DeleteLocalRef(netplay_player_class); + const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics"); s_analytics_class = reinterpret_cast(env->NewGlobalRef(analytics_class)); s_get_analytics_value = env->GetStaticMethodID(s_analytics_class, "getValue", @@ -892,6 +918,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_game_file_cache_class); env->DeleteGlobalRef(s_game_file_cache_manager_class); env->DeleteGlobalRef(s_netplay_class); + env->DeleteGlobalRef(s_netplay_player_class); env->DeleteGlobalRef(s_analytics_class); env->DeleteGlobalRef(s_pair_class); env->DeleteGlobalRef(s_hash_map_class); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index d59d1ca9f08a..0e393f919ad0 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -36,6 +36,10 @@ jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnConnectionError(); +jmethodID GetNetplayUpdate(); + +jclass GetNetplayPlayerClass(); +jmethodID GetNetplayPlayerConstructor(); jclass GetPairClass(); jmethodID GetPairConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index baa38de18d2d..478052e36c38 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -24,7 +24,41 @@ void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + if (!client) + return; + + const std::vector players = client->GetPlayers(); + + jobjectArray player_array = + env->NewObjectArray(static_cast(players.size()), IDCache::GetNetplayPlayerClass(), nullptr); + + for (jsize i = 0; i < static_cast(players.size()); i++) + { + const NetPlay::Player* player = players[i]; + const std::string mapping = NetPlay::GetPlayerMappingString( + player->pid, client->GetPadMapping(), client->GetGBAConfig(), client->GetWiimoteMapping()); + jobject player_obj = env->NewObject( + IDCache::GetNetplayPlayerClass(), IDCache::GetNetplayPlayerConstructor(), + static_cast(player->pid), + ToJString(env, player->name), + ToJString(env, player->revision), + static_cast(player->ping), + static_cast(player->IsHost()), + ToJString(env, mapping)); + env->SetObjectArrayElement(player_array, i, player_obj); + env->DeleteLocalRef(player_obj); + } + + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayUpdate(), player_array); + env->DeleteLocalRef(player_array); +} + void NetPlayUICallbacks::AppendChat(const std::string&) {} void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, From dfa3384281876b2fdc1c4d08ae1f4996033fbcae Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 19 May 2026 08:36:33 +0200 Subject: [PATCH 045/270] Netplay chat UI --- .../features/netplay/ui/NetplayScreen.kt | 259 +++++++++++++++--- .../dolphinemu/ui/theme/DolphinTheme.kt | 88 ++++++ .../app/src/main/res/values/strings.xml | 3 + 3 files changed, 315 insertions(+), 35 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index e4089dae4546..c3e4742d5de2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -2,15 +2,20 @@ package org.dolphinemu.dolphinemu.features.netplay.ui +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -21,13 +26,22 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -36,6 +50,9 @@ import androidx.compose.ui.unit.dp import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme +import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer +import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox +import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,29 +67,160 @@ fun NetplayScreen( navigationIcon = { IconButton(onClick = onBackClicked) { Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", ) } }, ) }, ) { innerPadding -> - Column( + val modifier = Modifier + .fillMaxSize() + .consumeWindowInsets(innerPadding) + .padding(innerPadding) + + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { + LandscapeContent( + players = players, + modifier = modifier + ) + } else { + PortraitContent( + players = players, + modifier = modifier + ) + } + } +} + +@Composable +private fun PortraitContent( + players: List, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + ) { + Chat( modifier = Modifier - .fillMaxSize() - .consumeWindowInsets(innerPadding) - .verticalScroll(rememberScrollState()) - .padding(innerPadding) + .fillMaxWidth() + .fillMaxHeight(0.3f) + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) + + MenuSpacer() + + PlayersAndSettings( + players = players, + modifier = Modifier + .weight(1f) .padding(horizontal = DolphinTheme.scaffoldPadding) + ) + } +} + +@Composable +private fun LandscapeContent( + players: List, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + ) { + Chat( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) + + PlayersAndSettings( + players = players, + modifier = Modifier + .weight(1f) + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) + } +} + +@Composable +private fun PlayersAndSettings( + players: List, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + ) { + PlayersTable( + rows = buildList { + add(listOf("Player", "Ping", "Mapping")) + addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) }) + repeat(4 - players.size) { add(listOf("", "", "")) } + }, + modifier = Modifier + .fillMaxWidth() + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun Chat( + modifier: Modifier, +) { + var showBottomSheet by remember { mutableStateOf(false) } + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showBottomSheet = false }, + sheetState = bottomSheetState, + modifier = Modifier + .statusBarsPadding() + ) { + LazyColumn( + reverseLayout = true, + contentPadding = PaddingValues(bottom = 4.dp), + modifier = Modifier + .weight(1f) + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) { + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + OutlinedTextField( + value = "", + onValueChange = {}, + modifier = Modifier + .weight(1f) + ) + TextButton( + onClick = {}, + ) { + Text(stringResource(R.string.netplay_chat_send)) + } + } + } + } + OutlinedBox( + onClick = { showBottomSheet = true }, + label = { Text(stringResource(R.string.netplay_chat_label)) }, + modifier = modifier + ) { + LazyColumn( + reverseLayout = true, + userScrollEnabled = false, + modifier = Modifier + .fillMaxSize() ) { - PlayersTable( - rows = buildList { - add(listOf("Player", "Ping", "Mapping")) - addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) }) - }, - modifier = Modifier.fillMaxWidth() - ) } } } @@ -137,25 +285,66 @@ private fun PlayersTable( @Preview @Composable private fun NetplayScreenPreview() { - NetplayScreen( - onBackClicked = {}, - players = listOf( - Player( - pid = 1, - name = "Player 1", - revision = "123", - ping = 2, - isHost = true, - mapping = "m1" - ), - Player( - pid = 2, - name = "Player 2", - revision = "123", - ping = 23, - isHost = false, - mapping = "m2" - ), - ), - ) + PreviewTheme(darkTheme = false) { + NetplayScreen( + onBackClicked = {}, + players = previewPlayers, + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NetplayScreenDarkPreview() { + PreviewTheme(darkTheme = true) { + NetplayScreen( + onBackClicked = {}, + players = previewPlayers, + ) + } +} + +@Preview(widthDp = 891, heightDp = 411) +@Composable +private fun LandscapeNetplayScreenPreview() { + PreviewTheme(darkTheme = false) { + NetplayScreen( + onBackClicked = {}, + players = previewPlayers, + ) + } } + +@Preview( + widthDp = 891, + heightDp = 411, + uiMode = Configuration.UI_MODE_NIGHT_YES +) +@Composable +private fun LandscapeNetplayScreenDarkPreview() { + PreviewTheme(darkTheme = true) { + NetplayScreen( + onBackClicked = {}, + players = previewPlayers, + ) + } +} + +private val previewPlayers = listOf( + Player( + pid = 1, + name = "Player 1", + revision = "123", + ping = 2, + isHost = true, + mapping = "m1" + ), + Player( + pid = 2, + name = "Player 2", + revision = "123", + ping = 23, + isHost = false, + mapping = "m2" + ), +) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index 24b0f99c0aba..d24b3d834f5c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -4,18 +4,31 @@ package org.dolphinemu.dolphinemu.ui.theme import android.content.Context import androidx.annotation.AttrRes +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.google.android.material.color.MaterialColors import androidx.appcompat.R as AppCompatR @@ -37,6 +50,17 @@ fun DolphinTheme(content: @Composable () -> Unit) { ) } +@Composable +fun PreviewTheme( + darkTheme: Boolean, + content: @Composable () -> Unit, +) { + MaterialTheme( + colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme(), + content = content + ) +} + private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme { fun attr(@AttrRes attr: Int) = Color(MaterialColors.getColor(this, attr, 0)) @@ -107,3 +131,67 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme { @Composable fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp)) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OutlinedBox( + onClick: () -> Unit, + label: @Composable () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Box( + modifier = modifier + .padding(top = 8.dp) + ) { + val interactionSource = remember { MutableInteractionSource() } + OutlinedTextFieldDefaults.DecorationBox( + value = "chatText", + innerTextField = { + Box( + modifier = Modifier + .fillMaxSize() + ) { + content() + Box( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + .background( + Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface, + Color.Transparent + ) + ) + ) + ) + } + }, + enabled = true, + singleLine = false, + contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp), + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + label = { label() }, + container = { + OutlinedTextFieldDefaults.Container( + enabled = true, + isError = false, + interactionSource = interactionSource, + colors = OutlinedTextFieldDefaults.colors(), + ) + } + ) + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.extraSmall) + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + ) + } +} diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 903a414af380..bd1acb0bfb91 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -996,4 +996,7 @@ It can efficiently compress both junk data and encrypted Wii data. Host code Port Netplay + Start + Chat + Send From c32732714ed8c782bfab7c621f41452469621c6f Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 14 Apr 2026 09:57:01 +0200 Subject: [PATCH 046/270] Implement more NetPlayUICallbacks Includes chat, game changes, pad buffer changes and host input authority. Merges them all into the chat window. --- .../dolphinemu/features/netplay/Netplay.kt | 98 +++++++++++++ .../features/netplay/model/NetplayMessage.kt | 29 ++++ .../netplay/model/NetplayViewModel.kt | 18 +++ .../features/netplay/ui/NetplayActivity.kt | 3 + .../features/netplay/ui/NetplayScreen.kt | 137 +++++++++++++----- .../app/src/main/res/values/strings.xml | 4 + Source/Android/jni/AndroidCommon/IDCache.cpp | 32 ++++ Source/Android/jni/AndroidCommon/IDCache.h | 4 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 31 +++- Source/Android/jni/NetPlay/Netplay.cpp | 8 + 10 files changed, 324 insertions(+), 40 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index ceda99505107..88baa9f49588 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -4,19 +4,31 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType +import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player +//TODO add other necessary @Keep annotations +//TODO clear boot session data at appropriate time object Netplay { @Keep private var netPlayClientPointer: Long = 0 @@ -24,6 +36,8 @@ object Netplay { @Keep private var bootSessionDataPointer: Long = 0 + private var sessionScope: CoroutineScope? = null + val isLaunching: Boolean get() = bootSessionDataPointer != 0L @@ -33,13 +47,51 @@ object Netplay { private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() + private val _messages = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val messages = _messages.asSharedFlow() + private val _players = MutableSharedFlow>( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val players = _players.asSharedFlow().distinctUntilChanged() + private val _chatMessages = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val chatMessages = _chatMessages.asSharedFlow() + + private val _game = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val game = _game.asSharedFlow() + + private val _hostInputAuthorityEnabled = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val hostInputAuthorityEnabled = _hostInputAuthorityEnabled.asSharedFlow() + + private val _padBuffer = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val padBuffer = _padBuffer.asSharedFlow() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { + val scope = createSessionScope() + + // Gather all messages that should appear in the chat window. + mergeMessages() + .runningFold(emptyList()) { acc, msg -> listOf(msg) + acc } + .onEach { _messages.tryEmit(it) } + .launchIn(scope) + netPlayClientPointer = Join() val isConnected = netPlayClientPointer != 0L && isClientConnected() @@ -62,13 +114,29 @@ object Netplay { @OptIn(ExperimentalCoroutinesApi::class) private fun releaseNetplayClient() { + sessionScope?.cancel() + sessionScope = null + if (netPlayClientPointer != 0L) { ReleaseNetplayClient() netPlayClientPointer = 0 } + _launchGame.flush() _connectionErrors.flush() _players.resetReplayCache() + _messages.resetReplayCache() + _chatMessages.resetReplayCache() + _game.resetReplayCache() + _hostInputAuthorityEnabled.resetReplayCache() + _padBuffer.resetReplayCache() + } + + private fun createSessionScope(): CoroutineScope { + sessionScope?.cancel() + return CoroutineScope(SupervisorJob() + Dispatchers.IO).also { + sessionScope = it + } } @JvmStatic @@ -77,9 +145,19 @@ object Netplay { @JvmStatic external fun isClientConnected(): Boolean + @JvmStatic + external fun sendMessage(message: String) + @JvmStatic private external fun ReleaseNetplayClient() + private fun mergeMessages(): Flow = merge( + chatMessages.map { NetplayMessage.Chat(it) }, + game.map { NetplayMessage.GameChanged(it) }, + hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, + padBuffer.map { NetplayMessage.BufferChanged(it) }, + ) + // NetPlayUI callbacks @JvmStatic @@ -98,6 +176,26 @@ object Netplay { _players.tryEmit(players.toList()) } + @JvmStatic + fun onChatMessageReceived(message: String) { + _chatMessages.tryEmit(message) + } + + @JvmStatic + fun onHostInputAuthorityChanged(enabled: Boolean) { + _hostInputAuthorityEnabled.tryEmit(enabled) + } + + @JvmStatic + fun onGameChanged(game: String) { + _game.tryEmit(game) + } + + @JvmStatic + fun onPadBufferChanged(buffer: Int) { + _padBuffer.tryEmit(buffer) + } + // Settings @JvmStatic diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt new file mode 100644 index 000000000000..41ab8997bdb9 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt @@ -0,0 +1,29 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +import android.content.Context +import org.dolphinemu.dolphinemu.R + +sealed class NetplayMessage { + abstract fun message(context: Context): String + + class Chat(private val chatMessage: String) : NetplayMessage() { + override fun message(context: Context) = chatMessage + } + + class GameChanged(private val game: String) : NetplayMessage() { + override fun message(context: Context) = + context.getString(R.string.netplay_message_game_changed, game) + } + + class HostInputAuthorityChanged(private val hostInputAuthorityEnabled: Boolean) : NetplayMessage() { + override fun message(context: Context) = context.getString( + R.string.netplay_message_host_input_authority_changed, + if (hostInputAuthorityEnabled) "enabled" else "disabled" + ) + } + + class BufferChanged(private val buffer: Int) : NetplayMessage() { + override fun message(context: Context) = + context.getString(R.string.netplay_message_buffer_changed, buffer) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 942dc0d0dc1c..cc9dc00ae041 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -2,6 +2,8 @@ package org.dolphinemu.dolphinemu.features.netplay.model +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay +//TODO save settings class NetplayViewModel : ViewModel() { val launchGame = Netplay.launchGame @@ -23,12 +26,27 @@ class NetplayViewModel : ViewModel() { val players = Netplay.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + val messages = Netplay.messages + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + val game = Netplay.game + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") + init { if (!Netplay.isClientConnected()) { _goBack.trySend(Unit) } } + fun sendMessage(message: String) { + val trimmedMessage = message.trim() + if (trimmedMessage.isEmpty()) { + return + } + + Netplay.sendMessage(trimmedMessage) + } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index 5a91ae5b91a5..c4d0ded675ce 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -44,6 +44,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { DolphinTheme { NetplayScreen( onBackClicked = { finish() }, + messages = viewModel.messages.collectAsState().value, + onSendMessage = viewModel::sendMessage, + game = viewModel.game.collectAsState().value, players = viewModel.players.collectAsState().value, ) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index c3e4742d5de2..37cab3217c77 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -16,7 +16,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -42,12 +45,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer @@ -58,6 +64,9 @@ import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme @Composable fun NetplayScreen( onBackClicked: () -> Unit, + messages: List, + onSendMessage: (String) -> Unit, + game: String, players: List, ) { Scaffold( @@ -82,11 +91,17 @@ fun NetplayScreen( if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( + messages = messages, + onSendMessage = onSendMessage, + game = game, players = players, modifier = modifier ) } else { PortraitContent( + messages = messages, + onSendMessage = onSendMessage, + game = game, players = players, modifier = modifier ) @@ -96,6 +111,9 @@ fun NetplayScreen( @Composable private fun PortraitContent( + messages: List, + onSendMessage: (String) -> Unit, + game: String, players: List, modifier: Modifier = Modifier, ) { @@ -103,6 +121,8 @@ private fun PortraitContent( modifier = modifier ) { Chat( + messages = messages, + onSendMessage = onSendMessage, modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.3f) @@ -112,6 +132,7 @@ private fun PortraitContent( MenuSpacer() PlayersAndSettings( + game = game, players = players, modifier = Modifier .weight(1f) @@ -122,6 +143,9 @@ private fun PortraitContent( @Composable private fun LandscapeContent( + messages: List, + onSendMessage: (String) -> Unit, + game: String, players: List, modifier: Modifier = Modifier, ) { @@ -129,6 +153,8 @@ private fun LandscapeContent( modifier = modifier ) { Chat( + messages = messages, + onSendMessage = onSendMessage, modifier = Modifier .weight(1f) .fillMaxHeight() @@ -136,6 +162,7 @@ private fun LandscapeContent( ) PlayersAndSettings( + game = game, players = players, modifier = Modifier .weight(1f) @@ -146,6 +173,7 @@ private fun LandscapeContent( @Composable private fun PlayersAndSettings( + game: String, players: List, modifier: Modifier = Modifier, ) { @@ -153,6 +181,17 @@ private fun PlayersAndSettings( modifier = modifier .verticalScroll(rememberScrollState()) ) { + OutlinedTextField( + value = game, + onValueChange = {}, + label = { Text(stringResource(R.string.netplay_game_label)) }, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + ) + + MenuSpacer() + PlayersTable( rows = buildList { add(listOf("Player", "Ping", "Mapping")) @@ -168,8 +207,24 @@ private fun PlayersAndSettings( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Chat( + messages: List, + onSendMessage: (String) -> Unit, modifier: Modifier, ) { + val context = LocalContext.current + + fun LazyListScope.messages() { + items(messages.size) { index -> + Text(text = messages[index].message(context)) + } + } + + var draftMessage by remember { mutableStateOf("") } + val submitMessage = { + onSendMessage(draftMessage) + draftMessage = "" + } + var showBottomSheet by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -187,6 +242,7 @@ private fun Chat( .weight(1f) .padding(horizontal = DolphinTheme.scaffoldPadding) ) { + messages() } Row( @@ -197,19 +253,23 @@ private fun Chat( .padding(horizontal = 8.dp) ) { OutlinedTextField( - value = "", - onValueChange = {}, + value = draftMessage, + onValueChange = { draftMessage = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), + keyboardActions = KeyboardActions(onSend = { submitMessage() }), modifier = Modifier .weight(1f) ) TextButton( - onClick = {}, + onClick = submitMessage, + enabled = draftMessage.isNotBlank(), ) { Text(stringResource(R.string.netplay_chat_send)) } } } } + OutlinedBox( onClick = { showBottomSheet = true }, label = { Text(stringResource(R.string.netplay_chat_label)) }, @@ -221,6 +281,7 @@ private fun Chat( modifier = Modifier .fillMaxSize() ) { + messages() } } } @@ -286,10 +347,7 @@ private fun PlayersTable( @Composable private fun NetplayScreenPreview() { PreviewTheme(darkTheme = false) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } @@ -297,10 +355,7 @@ private fun NetplayScreenPreview() { @Composable private fun NetplayScreenDarkPreview() { PreviewTheme(darkTheme = true) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } @@ -308,10 +363,7 @@ private fun NetplayScreenDarkPreview() { @Composable private fun LandscapeNetplayScreenPreview() { PreviewTheme(darkTheme = false) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } @@ -320,31 +372,42 @@ private fun LandscapeNetplayScreenPreview() { heightDp = 411, uiMode = Configuration.UI_MODE_NIGHT_YES ) + @Composable private fun LandscapeNetplayScreenDarkPreview() { PreviewTheme(darkTheme = true) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } -private val previewPlayers = listOf( - Player( - pid = 1, - name = "Player 1", - revision = "123", - ping = 2, - isHost = true, - mapping = "m1" - ), - Player( - pid = 2, - name = "Player 2", - revision = "123", - ping = 23, - isHost = false, - mapping = "m2" - ), -) +@Composable +private fun PreviewNetplayScreen() { + NetplayScreen( + onBackClicked = {}, + players = listOf( + Player( + pid = 1, + name = "Player 1", + revision = "123", + ping = 2, + isHost = true, + mapping = "m1" + ), + Player( + pid = 2, + name = "Player 2", + revision = "123", + ping = 23, + isHost = false, + mapping = "m2" + ), + ), + messages = buildList { + repeat(5) { + add(NetplayMessage.Chat("Hello")) + } + }, + onSendMessage = {}, + game = "Game name", + ) +} diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index bd1acb0bfb91..fa2d5886526c 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -999,4 +999,8 @@ It can efficiently compress both junk data and encrypted Wii data. Start Chat Send + Game changed to %1$s + Buffer size changed to %1$d + "Host input authority %1$s" + Game diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index b45849bf5dc8..0ea1b77c5482 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -33,6 +33,10 @@ static jfieldID s_net_play_client_pointer; static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_connection_error; +static jmethodID s_netplay_on_game_changed; +static jmethodID s_netplay_on_host_input_authority_changed; +static jmethodID s_netplay_on_pad_buffer_changed; +static jmethodID s_netplay_on_chat_message_received; static jmethodID s_netplay_update; static jclass s_netplay_player_class; @@ -268,6 +272,26 @@ jmethodID GetNetplayOnConnectionError() return s_netplay_on_connection_error; } +jmethodID GetNetplayOnGameChanged() +{ + return s_netplay_on_game_changed; +} + +jmethodID GetNetplayOnHostInputAuthorityChanged() +{ + return s_netplay_on_host_input_authority_changed; +} + +jmethodID GetNetplayOnPadBufferChanged() +{ + return s_netplay_on_pad_buffer_changed; +} + +jmethodID GetNetplayOnChatMessageReceived() +{ + return s_netplay_on_chat_message_received; +} + jmethodID GetNetplayUpdate() { return s_netplay_update; @@ -694,6 +718,14 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); + s_netplay_on_game_changed = + env->GetStaticMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); + s_netplay_on_host_input_authority_changed = + env->GetStaticMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); + s_netplay_on_pad_buffer_changed = + env->GetStaticMethodID(netplay_class, "onPadBufferChanged", "(I)V"); + s_netplay_on_chat_message_received = + env->GetStaticMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); env->DeleteLocalRef(netplay_class); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0e393f919ad0..f61c0265d9b4 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -36,6 +36,10 @@ jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnConnectionError(); +jmethodID GetNetplayOnGameChanged(); +jmethodID GetNetplayOnHostInputAuthorityChanged(); +jmethodID GetNetplayOnPadBufferChanged(); +jmethodID GetNetplayOnChatMessageReceived(); jmethodID GetNetplayUpdate(); jclass GetNetplayPlayerClass(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 478052e36c38..ee8443b958ff 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -59,13 +59,23 @@ void NetPlayUICallbacks::Update() env->DeleteLocalRef(player_array); } -void NetPlayUICallbacks::AppendChat(const std::string&) {} +void NetPlayUICallbacks::AppendChat(const std::string& message) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnChatMessageReceived(), + ToJString(env, message)); +} void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, const std::string& netplay_name) { m_current_game_identifier = sync_identifier; m_current_game_name = netplay_name; + + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnGameChanged(), + ToJString(env, netplay_name)); } void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} @@ -86,8 +96,23 @@ void NetPlayUICallbacks::OnMsgStopGame() {} void NetPlayUICallbacks::OnMsgPowerButton() {} void NetPlayUICallbacks::OnPlayerConnect(const std::string&) {} void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} -void NetPlayUICallbacks::OnPadBufferChanged(u32) {} -void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool) {} + +void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) +{ + //TODO handle host input authority = true + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnPadBufferChanged(), + static_cast(buffer)); +} + +void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnHostInputAuthorityChanged(), + static_cast(enabled)); +} + void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} void NetPlayUICallbacks::OnConnectionLost() {} diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index c5fdd21ad5ee..75fb10ef310e 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -136,6 +136,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv return static_cast(GetPointer(env)->IsConnected()); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_sendMessage(JNIEnv* env, jclass, + jstring jmessage) +{ + if (auto* client = GetPointer(env)) + client->SendChatMessage(GetJString(env, jmessage)); +} + JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) { From e49451b28e9356e9378bf841ffdefa2ac3aa1b75 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 15 Apr 2026 22:23:37 +0200 Subject: [PATCH 047/270] Put players table in an OutlinedBox, makes the UI look more consistent. --- .../features/netplay/ui/NetplayScreen.kt | 28 +++++++--- .../dolphinemu/ui/theme/DolphinTheme.kt | 55 +++++++++++-------- .../app/src/main/res/values/strings.xml | 4 ++ 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 37cab3217c77..528c0d8a0ba4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -192,15 +192,25 @@ private fun PlayersAndSettings( MenuSpacer() - PlayersTable( - rows = buildList { - add(listOf("Player", "Ping", "Mapping")) - addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) }) - repeat(4 - players.size) { add(listOf("", "", "")) } - }, - modifier = Modifier - .fillMaxWidth() - ) + OutlinedBox( + label = { Text(stringResource(R.string.netplay_players_label)) }, + ) { + PlayersTable( + rows = buildList { + add( + listOf( + stringResource(R.string.netplay_players_name), + stringResource(R.string.netplay_players_ping), + stringResource(R.string.netplay_players_mapping), + ) + ) + addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) }) + repeat(4 - players.size) { add(listOf("", "", "")) } + }, + modifier = Modifier + .fillMaxWidth() + ) + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index d24b3d834f5c..cf768e6dc086 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -135,9 +135,10 @@ fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp)) @OptIn(ExperimentalMaterial3Api::class) @Composable fun OutlinedBox( - onClick: () -> Unit, label: @Composable () -> Unit, modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + fadeContentTop: Boolean = false, content: @Composable () -> Unit, ) { Box( @@ -153,24 +154,30 @@ fun OutlinedBox( .fillMaxSize() ) { content() - Box( - modifier = Modifier - .fillMaxWidth() - .height(16.dp) - .background( - Brush.verticalGradient( - colors = listOf( - MaterialTheme.colorScheme.surface, - Color.Transparent + if (fadeContentTop) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + .background( + Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface, + Color.Transparent + ) ) ) - ) - ) + ) + } } }, enabled = true, singleLine = false, - contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp), + contentPadding = if (fadeContentTop) { + OutlinedTextFieldDefaults.contentPadding(top = 0.dp) + } else { + OutlinedTextFieldDefaults.contentPadding() + }, visualTransformation = VisualTransformation.None, interactionSource = interactionSource, label = { label() }, @@ -183,15 +190,17 @@ fun OutlinedBox( ) } ) - Box( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.extraSmall) - .clickable( - interactionSource = interactionSource, - indication = LocalIndication.current, - onClick = onClick, - ) - ) + if (onClick != null) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.extraSmall) + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + ) + } } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index fa2d5886526c..838382494076 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1003,4 +1003,8 @@ It can efficiently compress both junk data and encrypted Wii data. Buffer size changed to %1$d "Host input authority %1$s" Game + Players + Name + Ping + Mapping From 65e4c6015860c650d6afa844e86ca6f1ecd2c15a Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 15 Apr 2026 22:23:02 +0200 Subject: [PATCH 048/270] Max buffer --- .../dolphinemu/features/netplay/Netplay.kt | 11 ++ .../netplay/model/NetplayViewModel.kt | 14 ++ .../features/netplay/ui/NetplayActivity.kt | 3 + .../features/netplay/ui/NetplayScreen.kt | 150 +++++++++++++++++- .../app/src/main/res/values/strings.xml | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 1 - Source/Android/jni/NetPlay/Netplay.cpp | 21 +++ 7 files changed, 199 insertions(+), 2 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 88baa9f49588..319929e71e2f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -148,6 +148,9 @@ object Netplay { @JvmStatic external fun sendMessage(message: String) + @JvmStatic + external fun adjustPadBufferSize(buffer: Int) + @JvmStatic private external fun ReleaseNetplayClient() @@ -193,6 +196,8 @@ object Netplay { @JvmStatic fun onPadBufferChanged(buffer: Int) { + // Only for remote pad buffer settings. Ignore local max buffer changes. + if (_hostInputAuthorityEnabled.replayCache.firstOrNull() == true) return _padBuffer.tryEmit(buffer) } @@ -240,6 +245,12 @@ object Netplay { @JvmStatic external fun getIndexPassword(): String + @JvmStatic + external fun getClientBufferSize(): Int + + @JvmStatic + external fun setClientBufferSize(buffer: Int) + suspend fun saveSetup( nickname: String, connectionType: ConnectionType, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index cc9dc00ae041..9da56d0c48e0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -10,7 +10,9 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -32,6 +34,12 @@ class NetplayViewModel : ViewModel() { val game = Netplay.game .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") + val hostInputAuthority = Netplay.hostInputAuthorityEnabled + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + + private val _maxBuffer = MutableStateFlow(Netplay.getClientBufferSize()) + val maxBuffer = _maxBuffer.asStateFlow() + init { if (!Netplay.isClientConnected()) { _goBack.trySend(Unit) @@ -47,6 +55,12 @@ class NetplayViewModel : ViewModel() { Netplay.sendMessage(trimmedMessage) } + fun setMaxBuffer(buffer: Int) { + _maxBuffer.value = buffer + Netplay.setClientBufferSize(buffer) + Netplay.adjustPadBufferSize(buffer) + } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index c4d0ded675ce..fec0823456ea 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -48,6 +48,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, players = viewModel.players.collectAsState().value, + hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, + maxBuffer = viewModel.maxBuffer.collectAsState().value, + onMaxBufferChanged = viewModel::setMaxBuffer, ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 528c0d8a0ba4..73db80633715 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -8,25 +8,32 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.ModalBottomSheet @@ -48,8 +55,12 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.dolphinemu.dolphinemu.R @@ -67,6 +78,9 @@ fun NetplayScreen( messages: List, onSendMessage: (String) -> Unit, game: String, + hostInputAuthorityEnabled: Boolean, + maxBuffer: Int, + onMaxBufferChanged: (Int) -> Unit, players: List, ) { Scaffold( @@ -95,6 +109,9 @@ fun NetplayScreen( onSendMessage = onSendMessage, game = game, players = players, + hostInputAuthorityEnabled = hostInputAuthorityEnabled, + maxBuffer = maxBuffer, + onMaxBufferChanged = onMaxBufferChanged, modifier = modifier ) } else { @@ -103,6 +120,9 @@ fun NetplayScreen( onSendMessage = onSendMessage, game = game, players = players, + hostInputAuthorityEnabled = hostInputAuthorityEnabled, + maxBuffer = maxBuffer, + onMaxBufferChanged = onMaxBufferChanged, modifier = modifier ) } @@ -115,6 +135,9 @@ private fun PortraitContent( onSendMessage: (String) -> Unit, game: String, players: List, + hostInputAuthorityEnabled: Boolean, + maxBuffer: Int, + onMaxBufferChanged: (Int) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -134,9 +157,12 @@ private fun PortraitContent( PlayersAndSettings( game = game, players = players, + hostInputAuthorityEnabled = hostInputAuthorityEnabled, + maxBuffer = maxBuffer, + onMaxBufferChanged = onMaxBufferChanged, modifier = Modifier .weight(1f) - .padding(horizontal = DolphinTheme.scaffoldPadding) + .padding(horizontal = DolphinTheme.scaffoldPadding), ) } } @@ -147,6 +173,9 @@ private fun LandscapeContent( onSendMessage: (String) -> Unit, game: String, players: List, + hostInputAuthorityEnabled: Boolean, + maxBuffer: Int, + onMaxBufferChanged: (Int) -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -164,6 +193,9 @@ private fun LandscapeContent( PlayersAndSettings( game = game, players = players, + hostInputAuthorityEnabled = hostInputAuthorityEnabled, + maxBuffer = maxBuffer, + onMaxBufferChanged = onMaxBufferChanged, modifier = Modifier .weight(1f) .padding(horizontal = DolphinTheme.scaffoldPadding) @@ -175,6 +207,9 @@ private fun LandscapeContent( private fun PlayersAndSettings( game: String, players: List, + hostInputAuthorityEnabled: Boolean, + maxBuffer: Int, + onMaxBufferChanged: (Int) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -211,6 +246,16 @@ private fun PlayersAndSettings( .fillMaxWidth() ) } + + if (hostInputAuthorityEnabled) { + MenuSpacer() + + BufferInput( + value = maxBuffer, + onValueChange = onMaxBufferChanged, + label = stringResource(R.string.netplay_max_buffer), + ) + } } } @@ -353,6 +398,106 @@ private fun PlayersTable( } } +@Composable +private fun BufferInput( + value: Int, + onValueChange: (Int) -> Unit, + label: String, +) { + val range = 0..99 + var maybeEmptyValue by remember(value) { + mutableStateOf("$value") + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + OutlinedTextField( + value = TextFieldValue( + text = maybeEmptyValue, + selection = TextRange(maybeEmptyValue.length) + ), + onValueChange = { newValue -> + if (newValue.text.isEmpty()) { + maybeEmptyValue = newValue.text + return@OutlinedTextField + } + newValue.text.toIntOrNull()?.let { + if (it in range) { + onValueChange(it) + } + } + }, + label = { Text(label) }, + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center + ), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + singleLine = true, + modifier = Modifier + .weight(1f) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Button( + onClick = { + if (maybeEmptyValue.isEmpty()) { + maybeEmptyValue = "0" + onValueChange(0) + } else { + val newValue = value - 1 + if (newValue in range) { + onValueChange(newValue) + } + } + }, + shape = RoundedCornerShape( + topStartPercent = 50, + topEndPercent = 0, + bottomEndPercent = 0, + bottomStartPercent = 50, + ), + modifier = Modifier + .height(60.dp) + .padding(top = 8.dp) + ) { + Icon(Icons.Filled.Remove, contentDescription = "Back") + } + + Spacer(modifier = Modifier.width(2.dp)) + + Button( + onClick = { + if (maybeEmptyValue.isEmpty()) { + maybeEmptyValue = "0" + onValueChange(0) + } else { + val newValue = value + 1 + if (newValue in range) { + onValueChange(newValue) + } + } + }, + shape = RoundedCornerShape( + topStartPercent = 0, + topEndPercent = 50, + bottomEndPercent = 50, + bottomStartPercent = 0, + ), + modifier = Modifier + .height(60.dp) + .padding(top = 8.dp) + ) { + Icon(Icons.Filled.Add, contentDescription = "Back") + } + } +} + @Preview @Composable private fun NetplayScreenPreview() { @@ -419,5 +564,8 @@ private fun PreviewNetplayScreen() { }, onSendMessage = {}, game = "Game name", + hostInputAuthorityEnabled = true, + maxBuffer = 10, + onMaxBufferChanged = {}, ) } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 838382494076..0bd19d654bec 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1007,4 +1007,5 @@ It can efficiently compress both junk data and encrypted Wii data. Name Ping Mapping + Max buffer diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index ee8443b958ff..8f80d336b0de 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -99,7 +99,6 @@ void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) { - //TODO handle host input authority = true JNIEnv* env = IDCache::GetEnvForThread(); env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnPadBufferChanged(), static_cast(buffer)); diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 75fb10ef310e..8b4c7fff65a8 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -103,6 +103,19 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexPassword(JNIEnv* return ToJString(env, Config::Get(Config::NETPLAY_INDEX_PASSWORD)); } +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getClientBufferSize(JNIEnv*, jclass) +{ + return static_cast(Config::Get(Config::NETPLAY_CLIENT_BUFFER_SIZE)); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_setClientBufferSize(JNIEnv*, jclass, + jint buffer) +{ + Config::SetBase(Config::NETPLAY_CLIENT_BUFFER_SIZE, static_cast(buffer)); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup( JNIEnv* env, jclass, jstring jnickname, jstring traversalChoice, jstring jaddress, @@ -144,6 +157,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_sendMessage(JNIEnv* env, client->SendChatMessage(GetJString(env, jmessage)); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_adjustPadBufferSize(JNIEnv* env, jclass, + jint buffer) +{ + if (auto* client = GetPointer(env)) + client->AdjustPadBufferSize(static_cast(buffer)); +} + JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) { From 86956b21bea7f6450f180fddf2ffcb2e1077a3b6 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sun, 19 Apr 2026 20:07:37 +0200 Subject: [PATCH 049/270] Settings refactor Remove the big saveSetup function and set individual settings immediately after being changed in the UI. Group them all under Netplay.Settings --- .../dolphinemu/features/netplay/Netplay.kt | 107 ++++------------ .../netplay/model/NetplaySetupViewModel.kt | 23 ++-- .../netplay/model/NetplayViewModel.kt | 4 +- Source/Android/jni/NetPlay/Netplay.cpp | 118 +++++++----------- 4 files changed, 83 insertions(+), 169 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 319929e71e2f..94de9bd98c1b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -202,99 +202,46 @@ object Netplay { } // Settings + object Settings { + @JvmStatic + external fun getNickname(): String - @JvmStatic - external fun getNickname(): String - - fun getConnectionType(): ConnectionType = ConnectionType.all - .find { it.configValue == getTraversalChoice() } ?: throw IllegalStateException() - - @JvmStatic - external fun getTraversalChoice(): String + @JvmStatic + external fun setNickname(nickname: String) - @JvmStatic - external fun getAddress(): String + fun getConnectionType(): ConnectionType = ConnectionType.all + .find { it.configValue == getTraversalChoice() } ?: throw IllegalStateException() - @JvmStatic - external fun getHostCode(): String + @JvmStatic + external fun getTraversalChoice(): String - @JvmStatic - external fun getConnectPort(): Int - - @JvmStatic - external fun getHostPort(): Int - - @JvmStatic - external fun getUseUpnp(): Boolean + @JvmStatic + external fun setTraversalChoice(traversalChoice: String) - @JvmStatic - external fun getEnableChunkedUploadLimit(): Boolean + @JvmStatic + external fun getAddress(): String - @JvmStatic - external fun getChunkedUploadLimit(): Int + @JvmStatic + external fun setAddress(address: String) - @JvmStatic - external fun getUseIndex(): Boolean + @JvmStatic + external fun getHostCode(): String - @JvmStatic - external fun getIndexRegion(): String + @JvmStatic + external fun setHostCode(hostCode: String) - @JvmStatic - external fun getIndexName(): String + @JvmStatic + external fun getConnectPort(): Int - @JvmStatic - external fun getIndexPassword(): String + @JvmStatic + external fun setConnectPort(port: Int) - @JvmStatic - external fun getClientBufferSize(): Int + @JvmStatic + external fun getClientBufferSize(): Int - @JvmStatic - external fun setClientBufferSize(buffer: Int) - - suspend fun saveSetup( - nickname: String, - connectionType: ConnectionType, - address: String, - hostCode: String, - connectPort: Int, - ) = withContext(Dispatchers.IO) { - SaveSetup( - nickname = nickname, - traversalChoice = connectionType.configValue, - address = address, - hostCode = hostCode, - connectPort = connectPort, - hostPort = 2626, - useUpnp = false, - useListenPort = false, - listenPort = 2626, - enableChunkedUploadLimit = false, - chunkedUploadLimit = 3000, - useIndex = false, - indexRegion = "", - indexName = "", - indexPassword = "", - ) + @JvmStatic + external fun setClientBufferSize(buffer: Int) } - - @JvmStatic - external fun SaveSetup( - nickname: String, - traversalChoice: String, - address: String, - hostCode: String, - connectPort: Int, - hostPort: Int, - useUpnp: Boolean, - useListenPort: Boolean, - listenPort: Int, - enableChunkedUploadLimit: Boolean, - chunkedUploadLimit: Int, - useIndex: Boolean, - indexRegion: String, - indexName: String, - indexPassword: String, - ) } private fun Channel.flush() { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 19175e4df85b..d1bd202c2f5f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -19,19 +19,19 @@ class NetplaySetupViewModel : ViewModel() { private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) val connectionRole = _connectionRole.asStateFlow() - private val _nickname = MutableStateFlow(Netplay.getNickname()) + private val _nickname = MutableStateFlow(Netplay.Settings.getNickname()) val nickname = _nickname.asStateFlow() - private val _connectionType = MutableStateFlow(Netplay.getConnectionType()) + private val _connectionType = MutableStateFlow(Netplay.Settings.getConnectionType()) val connectionType = _connectionType.asStateFlow() - private val _ipAddress = MutableStateFlow(Netplay.getAddress()) + private val _ipAddress = MutableStateFlow(Netplay.Settings.getAddress()) val ipAddress = _ipAddress.asStateFlow() - private val _hostCode = MutableStateFlow(Netplay.getHostCode()) + private val _hostCode = MutableStateFlow(Netplay.Settings.getHostCode()) val hostCode = _hostCode.asStateFlow() - private val _connectPort = MutableStateFlow(Netplay.getConnectPort().toString()) + private val _connectPort = MutableStateFlow(Netplay.Settings.getConnectPort().toString()) val connectPort = _connectPort.asStateFlow() private val _showNetplayScreen = Channel(CONFLATED) @@ -52,25 +52,30 @@ class NetplaySetupViewModel : ViewModel() { fun setNickname(nickname: String) { _nickname.value = nickname + Netplay.Settings.setNickname(nickname) } fun setConnectionType(connectionType: ConnectionType) { _connectionType.value = connectionType + Netplay.Settings.setTraversalChoice(connectionType.configValue) } fun setIpAddress(ipAddress: String) { if (ipAddress.all { it.isDigit() || it == '.' }) { _ipAddress.value = ipAddress + Netplay.Settings.setAddress(ipAddress) } } fun setHostCode(hostCode: String) { _hostCode.value = hostCode + Netplay.Settings.setHostCode(hostCode) } fun setConnectPort(port: String) { if (port.all { it.isDigit() }) { _connectPort.value = port + port.toIntOrNull()?.let { Netplay.Settings.setConnectPort(it) } } } @@ -80,14 +85,6 @@ class NetplaySetupViewModel : ViewModel() { viewModelScope.launch { GameFileCacheManager.isLoading().asFlow().first { it == false } - Netplay.saveSetup( - nickname = nickname.value, - connectionType = connectionType.value, - address = ipAddress.value, - hostCode = hostCode.value, - connectPort = connectPort.value.toInt(), - ) - if (Netplay.join()) { _showNetplayScreen.trySend(Unit) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 9da56d0c48e0..217991f40c62 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -37,7 +37,7 @@ class NetplayViewModel : ViewModel() { val hostInputAuthority = Netplay.hostInputAuthorityEnabled .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) - private val _maxBuffer = MutableStateFlow(Netplay.getClientBufferSize()) + private val _maxBuffer = MutableStateFlow(Netplay.Settings.getClientBufferSize()) val maxBuffer = _maxBuffer.asStateFlow() init { @@ -57,7 +57,7 @@ class NetplayViewModel : ViewModel() { fun setMaxBuffer(buffer: Int) { _maxBuffer.value = buffer - Netplay.setClientBufferSize(buffer) + Netplay.Settings.setClientBufferSize(buffer) Netplay.adjustPadBufferSize(buffer) } diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 8b4c7fff65a8..9dd2743d5345 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -26,123 +26,93 @@ static NetPlay::NetPlayClient* GetPointer(JNIEnv* env) extern "C" { JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getNickname(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getNickname(JNIEnv* env, + jclass) { return ToJString(env, Config::Get(Config::NETPLAY_NICKNAME)); } -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getTraversalChoice(JNIEnv* env, jclass) +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setNickname(JNIEnv* env, + jclass, + jstring jnickname) { - return ToJString(env, Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE)); + Config::SetBase(Config::NETPLAY_NICKNAME, GetJString(env, jnickname)); } JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getAddress(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getTraversalChoice( + JNIEnv* env, jclass) { - return ToJString(env, Config::Get(Config::NETPLAY_ADDRESS)); + return ToJString(env, Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE)); } -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getHostCode(JNIEnv* env, jclass) +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setTraversalChoice( + JNIEnv* env, jclass, jstring jtraversalChoice) { - return ToJString(env, Config::Get(Config::NETPLAY_HOST_CODE)); + Config::SetBase(Config::NETPLAY_TRAVERSAL_CHOICE, GetJString(env, jtraversalChoice)); } -JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getConnectPort(JNIEnv*, jclass) +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getAddress(JNIEnv* env, + jclass) { - return static_cast(Config::Get(Config::NETPLAY_CONNECT_PORT)); + return ToJString(env, Config::Get(Config::NETPLAY_ADDRESS)); } -JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getHostPort(JNIEnv*, jclass) +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setAddress(JNIEnv* env, + jclass, + jstring jaddress) { - return static_cast(Config::Get(Config::NETPLAY_HOST_PORT)); + Config::SetBase(Config::NETPLAY_ADDRESS, GetJString(env, jaddress)); } -JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getUseUpnp(JNIEnv*, jclass) +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getHostCode(JNIEnv* env, + jclass) { - return static_cast(Config::Get(Config::NETPLAY_USE_UPNP)); + return ToJString(env, Config::Get(Config::NETPLAY_HOST_CODE)); } -JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getEnableChunkedUploadLimit(JNIEnv*, jclass) +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setHostCode(JNIEnv* env, + jclass, + jstring jhostCode) { - return static_cast(Config::Get(Config::NETPLAY_ENABLE_CHUNKED_UPLOAD_LIMIT)); + Config::SetBase(Config::NETPLAY_HOST_CODE, GetJString(env, jhostCode)); } JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getChunkedUploadLimit(JNIEnv*, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getConnectPort(JNIEnv*, + jclass) { - return static_cast(Config::Get(Config::NETPLAY_CHUNKED_UPLOAD_LIMIT)); -} - -JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getUseIndex(JNIEnv*, jclass) -{ - return static_cast(Config::Get(Config::NETPLAY_USE_INDEX)); -} - -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexRegion(JNIEnv* env, jclass) -{ - return ToJString(env, Config::Get(Config::NETPLAY_INDEX_REGION)); -} - -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexName(JNIEnv* env, jclass) -{ - return ToJString(env, Config::Get(Config::NETPLAY_INDEX_NAME)); + return static_cast(Config::Get(Config::NETPLAY_CONNECT_PORT)); } -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getIndexPassword(JNIEnv* env, jclass) +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setConnectPort(JNIEnv*, + jclass, + jint port) { - return ToJString(env, Config::Get(Config::NETPLAY_INDEX_PASSWORD)); + Config::SetBase(Config::NETPLAY_CONNECT_PORT, static_cast(port)); } JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_getClientBufferSize(JNIEnv*, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getClientBufferSize(JNIEnv*, + jclass) { return static_cast(Config::Get(Config::NETPLAY_CLIENT_BUFFER_SIZE)); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_setClientBufferSize(JNIEnv*, jclass, - jint buffer) +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setClientBufferSize( + JNIEnv*, jclass, jint buffer) { Config::SetBase(Config::NETPLAY_CLIENT_BUFFER_SIZE, static_cast(buffer)); } -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup( - JNIEnv* env, jclass, jstring jnickname, jstring traversalChoice, jstring jaddress, - jstring jhostCode, jint connectPort, jint hostPort, jboolean useUpnp, jboolean useListenPort, - jint listenPort, jboolean enableChunkedUploadLimit, jint chunkedUploadLimit, jboolean useIndex, - jstring jindexRegion, jstring jindexName, jstring jindexPassword) -{ - Config::ConfigChangeCallbackGuard config_guard; - - Config::SetBaseOrCurrent(Config::NETPLAY_NICKNAME, GetJString(env, jnickname)); - Config::SetBaseOrCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, GetJString(env, traversalChoice)); - Config::SetBaseOrCurrent(Config::NETPLAY_ADDRESS, GetJString(env, jaddress)); - Config::SetBaseOrCurrent(Config::NETPLAY_HOST_CODE, GetJString(env, jhostCode)); - Config::SetBaseOrCurrent(Config::NETPLAY_CONNECT_PORT, static_cast(connectPort)); - Config::SetBaseOrCurrent(Config::NETPLAY_HOST_PORT, static_cast(hostPort)); - Config::SetBaseOrCurrent(Config::NETPLAY_USE_UPNP, static_cast(useUpnp)); - Config::SetBaseOrCurrent(Config::NETPLAY_ENABLE_CHUNKED_UPLOAD_LIMIT, - static_cast(enableChunkedUploadLimit)); - Config::SetBaseOrCurrent(Config::NETPLAY_CHUNKED_UPLOAD_LIMIT, - static_cast(chunkedUploadLimit)); - Config::SetBaseOrCurrent(Config::NETPLAY_USE_INDEX, static_cast(useIndex)); - Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_REGION, GetJString(env, jindexRegion)); - Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_NAME, GetJString(env, jindexName)); - Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_PASSWORD, GetJString(env, jindexPassword)); - Config::SetBaseOrCurrent(Config::NETPLAY_LISTEN_PORT, static_cast(listenPort)); -} - JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass) { From 38b5c7370c47a6760bfce4cb8f9c45ab5d403dbb Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 22 Apr 2026 18:46:12 +0200 Subject: [PATCH 050/270] Implement StopGame callback and use it to finish the emulation activity --- .../dolphinemu/features/netplay/Netplay.kt | 13 +++++++++- .../dolphinemu/fragments/EmulationFragment.kt | 7 ++++++ Source/Android/jni/AndroidCommon/IDCache.cpp | 7 ++++++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 24 ++++++++++++++++++- .../Android/jni/NetPlay/NetPlayUICallbacks.h | 3 +++ 6 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 94de9bd98c1b..a90d1b00d8bb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -44,6 +44,9 @@ object Netplay { private val _launchGame = Channel(Channel.CONFLATED) val launchGame = _launchGame.receiveAsFlow() + private val _stopGame = Channel(Channel.CONFLATED) + val stopGame = _stopGame.receiveAsFlow() + private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() @@ -123,6 +126,7 @@ object Netplay { } _launchGame.flush() + _stopGame.flush() _connectionErrors.flush() _players.resetReplayCache() _messages.resetReplayCache() @@ -166,9 +170,16 @@ object Netplay { @JvmStatic fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { this.bootSessionDataPointer = bootSessionDataPointer + _stopGame.flush() _launchGame.trySend(gameFilePath) } + @Keep + @JvmStatic + fun onStopGame() { + _stopGame.trySend(Unit) + } + @JvmStatic fun onConnectionError(message: String) { _connectionErrors.trySend(message) @@ -244,6 +255,6 @@ object Netplay { } } -private fun Channel.flush() { +private fun Channel.flush() { while (this.tryReceive().isSuccess) Unit } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index 4ee4aee8aaa8..61a549395005 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -10,6 +10,9 @@ import android.view.SurfaceHolder import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding @@ -220,6 +223,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" } + lifecycleScope.launch { + Netplay.stopGame.first() + stopEmulation() + } NativeLibrary.RunNetPlay(paths, riivolution) } else { Log.debug("[EmulationFragment] Starting emulation thread.") diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 0ea1b77c5482..bc59fe00530f 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -32,6 +32,7 @@ static jclass s_netplay_class; static jfieldID s_net_play_client_pointer; static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; +static jmethodID s_netplay_on_stop_game; static jmethodID s_netplay_on_connection_error; static jmethodID s_netplay_on_game_changed; static jmethodID s_netplay_on_host_input_authority_changed; @@ -267,6 +268,11 @@ jmethodID GetNetplayOnBootGame() return s_netplay_on_boot_game; } +jmethodID GetNetplayOnStopGame() +{ + return s_netplay_on_stop_game; +} + jmethodID GetNetplayOnConnectionError() { return s_netplay_on_connection_error; @@ -717,6 +723,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J"); s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); + s_netplay_on_stop_game = env->GetStaticMethodID(netplay_class, "onStopGame", "()V"); s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = env->GetStaticMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index f61c0265d9b4..5646669f18dd 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -35,6 +35,7 @@ jclass GetNetplayClass(); jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); +jmethodID GetNetplayOnStopGame(); jmethodID GetNetplayOnConnectionError(); jmethodID GetNetplayOnGameChanged(); jmethodID GetNetplayOnHostInputAuthorityChanged(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 8f80d336b0de..dd25d5428037 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -4,6 +4,7 @@ #include "UICommon/GameFile.h" #include "NetPlayUICallbacks.h" #include "Core/Boot/Boot.h" +#include "Core/Core.h" #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" @@ -12,17 +13,38 @@ namespace NetPlay { NetPlayUICallbacks::NetPlayUICallbacks(std::vector> games) : m_games(std::move(games)) { + m_state_changed_hook = Core::AddOnStateChangedCallback([this](Core::State state) { + if ((state == Core::State::Uninitialized || state == Core::State::Stopping) && + !m_got_stop_request) + { + JNIEnv* env = IDCache::GetEnvForThread(); + auto* client = reinterpret_cast( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + if (client) + client->RequestStopGame(); + } + }); } NetPlayUICallbacks::~NetPlayUICallbacks() = default; void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr boot_session_data) { + m_got_stop_request = false; JNIEnv* env = IDCache::GetEnvForThread(); env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnBootGame(), ToJString(env, filename), reinterpret_cast(boot_session_data.release())); } -void NetPlayUICallbacks::StopGame() {} +void NetPlayUICallbacks::StopGame() +{ + if (m_got_stop_request) + return; + + m_got_stop_request = true; + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnStopGame()); +} + bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index acccfccabf48..10c1b1ac2249 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -5,6 +5,7 @@ #include #include +#include "Common/HookableEvent.h" #include "Core/NetPlayClient.h" #include "UICommon/GameFile.h" @@ -61,6 +62,8 @@ class NetPlayUICallbacks : public NetPlay::NetPlayUI { std::vector> m_games; NetPlay::SyncIdentifier m_current_game_identifier; std::string m_current_game_name; + Common::EventHook m_state_changed_hook; + bool m_got_stop_request = true; }; } // namespace NetPlay From b1b133b0361d5047b01d60dd492f0d5207921e42 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sat, 25 Apr 2026 17:40:40 +0200 Subject: [PATCH 051/270] Implement OnConnectionLost, show a dialog on the main netplay screen forcing the user to exit --- .../dolphinemu/features/netplay/Netplay.kt | 11 ++++++++- .../netplay/model/NetplayViewModel.kt | 2 ++ .../features/netplay/ui/NetplayActivity.kt | 1 + .../features/netplay/ui/NetplayScreen.kt | 24 ++++++++++++++++++- .../app/src/main/res/values/strings.xml | 1 + Source/Android/jni/AndroidCommon/IDCache.cpp | 7 ++++++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 7 +++++- 8 files changed, 51 insertions(+), 3 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index a90d1b00d8bb..7298795d71e5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -47,6 +47,9 @@ object Netplay { private val _stopGame = Channel(Channel.CONFLATED) val stopGame = _stopGame.receiveAsFlow() + private val _connectionLost = Channel(Channel.CONFLATED) + val connectionLost = _connectionLost.receiveAsFlow() + private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() @@ -63,7 +66,7 @@ object Netplay { val players = _players.asSharedFlow().distinctUntilChanged() private val _chatMessages = MutableSharedFlow( - replay = 1, + extraBufferCapacity = 32, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val chatMessages = _chatMessages.asSharedFlow() @@ -180,6 +183,12 @@ object Netplay { _stopGame.trySend(Unit) } + @Keep + @JvmStatic + fun onConnectionLost() { + _connectionLost.trySend(Unit) + } + @JvmStatic fun onConnectionError(message: String) { _connectionErrors.trySend(message) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 217991f40c62..955fb876ddbd 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -25,6 +25,8 @@ class NetplayViewModel : ViewModel() { private val _goBack = Channel(CONFLATED) val goBack = _goBack.receiveAsFlow() + val connectionLost = Netplay.connectionLost + val players = Netplay.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index fec0823456ea..463ad315067f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -44,6 +44,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { DolphinTheme { NetplayScreen( onBackClicked = { finish() }, + connectionLost = viewModel.connectionLost, messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 73db80633715..80914e952ce0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -43,6 +44,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -63,6 +65,8 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player @@ -75,6 +79,7 @@ import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme @Composable fun NetplayScreen( onBackClicked: () -> Unit, + connectionLost: Flow, messages: List, onSendMessage: (String) -> Unit, game: String, @@ -126,6 +131,22 @@ fun NetplayScreen( modifier = modifier ) } + + var showConnectionLostDialog by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + connectionLost.collect { showConnectionLostDialog = true } + } + if (showConnectionLostDialog) { + AlertDialog( + text = { Text(stringResource(R.string.netplay_connection_lost)) }, + confirmButton = { + TextButton(onClick = onBackClicked) { + Text(stringResource(R.string.ok)) + } + }, + onDismissRequest = onBackClicked, + ) + } } } @@ -246,7 +267,7 @@ private fun PlayersAndSettings( .fillMaxWidth() ) } - + if (hostInputAuthorityEnabled) { MenuSpacer() @@ -539,6 +560,7 @@ private fun LandscapeNetplayScreenDarkPreview() { private fun PreviewNetplayScreen() { NetplayScreen( onBackClicked = {}, + connectionLost = emptyFlow(), players = listOf( Player( pid = 1, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 0bd19d654bec..7b4311c99697 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1008,4 +1008,5 @@ It can efficiently compress both junk data and encrypted Wii data. Ping Mapping Max buffer + Netplay connection lost diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index bc59fe00530f..461576f21746 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -33,6 +33,7 @@ static jfieldID s_net_play_client_pointer; static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_stop_game; +static jmethodID s_netplay_on_connection_lost; static jmethodID s_netplay_on_connection_error; static jmethodID s_netplay_on_game_changed; static jmethodID s_netplay_on_host_input_authority_changed; @@ -273,6 +274,11 @@ jmethodID GetNetplayOnStopGame() return s_netplay_on_stop_game; } +jmethodID GetNetplayOnConnectionLost() +{ + return s_netplay_on_connection_lost; +} + jmethodID GetNetplayOnConnectionError() { return s_netplay_on_connection_error; @@ -724,6 +730,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_stop_game = env->GetStaticMethodID(netplay_class, "onStopGame", "()V"); + s_netplay_on_connection_lost = env->GetStaticMethodID(netplay_class, "onConnectionLost", "()V"); s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = env->GetStaticMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 5646669f18dd..c535fcb9ae11 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -36,6 +36,7 @@ jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnStopGame(); +jmethodID GetNetplayOnConnectionLost(); jmethodID GetNetplayOnConnectionError(); jmethodID GetNetplayOnGameChanged(); jmethodID GetNetplayOnHostInputAuthorityChanged(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index dd25d5428037..21dd77d01971 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -135,7 +135,12 @@ void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) } void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} -void NetPlayUICallbacks::OnConnectionLost() {} + +void NetPlayUICallbacks::OnConnectionLost() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionLost()); +} void NetPlayUICallbacks::OnConnectionError(const std::string& message) { From b52ffa3e8ba6a12d1321c1f1adc76cf7cdce834e Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Thu, 23 Apr 2026 15:25:07 +0200 Subject: [PATCH 052/270] Release boot session data during Netplay cleanup Boot session data is already handled when the game is booted so this is just fallback in case the game launch fails in some weird way. Add missing @Keep annotations to functions called from C++ --- .../dolphinemu/features/netplay/Netplay.kt | 17 +++++++++++++++-- Source/Android/jni/NetPlay/Netplay.cpp | 10 ++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 7298795d71e5..f4eb178cb40c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -27,8 +27,6 @@ import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player -//TODO add other necessary @Keep annotations -//TODO clear boot session data at appropriate time object Netplay { @Keep private var netPlayClientPointer: Long = 0 @@ -123,6 +121,11 @@ object Netplay { sessionScope?.cancel() sessionScope = null + if (bootSessionDataPointer != 0L) { + ReleaseBootSessionData() + bootSessionDataPointer = 0 + } + if (netPlayClientPointer != 0L) { ReleaseNetplayClient() netPlayClientPointer = 0 @@ -158,6 +161,9 @@ object Netplay { @JvmStatic external fun adjustPadBufferSize(buffer: Int) + @JvmStatic + private external fun ReleaseBootSessionData() + @JvmStatic private external fun ReleaseNetplayClient() @@ -170,6 +176,7 @@ object Netplay { // NetPlayUI callbacks + @Keep @JvmStatic fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { this.bootSessionDataPointer = bootSessionDataPointer @@ -189,31 +196,37 @@ object Netplay { _connectionLost.trySend(Unit) } + @Keep @JvmStatic fun onConnectionError(message: String) { _connectionErrors.trySend(message) } + @Keep @JvmStatic fun onUpdate(players: Array) { _players.tryEmit(players.toList()) } + @Keep @JvmStatic fun onChatMessageReceived(message: String) { _chatMessages.tryEmit(message) } + @Keep @JvmStatic fun onHostInputAuthorityChanged(enabled: Boolean) { _hostInputAuthorityEnabled.tryEmit(enabled) } + @Keep @JvmStatic fun onGameChanged(game: String) { _game.tryEmit(game) } + @Keep @JvmStatic fun onPadBufferChanged(buffer: Int) { // Only for remote pad buffer settings. Ignore local max buffer changes. diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 9dd2743d5345..e8f3deaa1705 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -8,6 +8,7 @@ #include #include "Common/CommonTypes.h" +#include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" #include "UICommon/GameFile.h" @@ -166,6 +167,15 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass return reinterpret_cast(client); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseBootSessionData(JNIEnv* env, jclass) +{ + auto* data = reinterpret_cast( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer())); + delete data; + env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseNetplayClient(JNIEnv* env, jclass) { From 480968f05e03df0c860e84ac294593db91d74296 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Fri, 24 Apr 2026 13:18:36 +0200 Subject: [PATCH 053/270] Dont try to load temporary save states when launching netplay An orientation change can trigger this code path immediately when a game starts. e.g. dolphin is portrait when launching the game but settings force gameplay to landscape. We want to avoid this and continue to the netplay launch code below. If the user backgrounds dolphin during netplay and then resumes after the process has died it will actually resume from the save state in single player mode, not sure if thats good or bad but fine for now. Netplay doesnt handle rotation very well, seems to go more and more out of sync the more rotations. --- .../org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index 61a549395005..49331d8a4cab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -208,7 +208,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (NativeLibrary.IsUninitialized()) { NativeLibrary.SetIsBooting() val emulationThread = Thread({ - if (loadPreviousTemporaryState) { + // Don't load temporary saves when launching Netplay, this path can trigger + // when a game starts due to orientation changes caused by a mismatch in menu + // vs emulation activity orientations. + if (loadPreviousTemporaryState && !Netplay.isLaunching) { Log.debug("[EmulationFragment] Starting emulation thread from previous state.") val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" From 8141a746713b87be595c94fe4598eb198395b8da Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sat, 25 Apr 2026 17:39:41 +0200 Subject: [PATCH 054/270] Show save transfer progress When transferring saves from the host. Equivalent of ChunkedProgressDialog in QT. --- .../dolphinemu/features/netplay/Netplay.kt | 45 +++++++ .../netplay/model/NetplayViewModel.kt | 4 +- .../netplay/model/SaveTransferProgress.kt | 13 ++ .../features/netplay/ui/NetplayActivity.kt | 1 + .../features/netplay/ui/NetplayScreen.kt | 118 ++++++++++++++++-- .../app/src/main/res/values/strings.xml | 1 + Source/Android/jni/AndroidCommon/IDCache.cpp | 24 ++++ Source/Android/jni/AndroidCommon/IDCache.h | 3 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 33 ++++- 9 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index f4eb178cb40c..882016dfa93b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -13,7 +13,9 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -26,6 +28,7 @@ import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player +import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress object Netplay { @Keep @@ -87,6 +90,9 @@ object Netplay { ) val padBuffer = _padBuffer.asSharedFlow() + private val _saveTransferProgress = MutableStateFlow(null) + val saveTransferProgress = _saveTransferProgress.asStateFlow() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { val scope = createSessionScope() @@ -140,6 +146,7 @@ object Netplay { _game.resetReplayCache() _hostInputAuthorityEnabled.resetReplayCache() _padBuffer.resetReplayCache() + _saveTransferProgress.value = null } private fun createSessionScope(): CoroutineScope { @@ -234,6 +241,44 @@ object Netplay { _padBuffer.tryEmit(buffer) } + @Keep + @JvmStatic + fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) { + val players = _players.replayCache.firstOrNull() + _saveTransferProgress.value = SaveTransferProgress( + title = title, + totalSize = dataSize, + playerProgresses = playerIds.map { playerId -> + SaveTransferProgress.PlayerProgress( + playerId = playerId, + name = players?.find { it.pid == playerId }?.name ?: "Invalid Player ID", + progress = 0, + ) + }, + ) + } + + @Keep + @JvmStatic + fun onSetChunkedProgress(playerId: Int, progress: Long) { + val current = _saveTransferProgress.value + _saveTransferProgress.value = current?.copy( + playerProgresses = current.playerProgresses.map { + if (it.playerId == playerId) { + it.copy(progress = progress) + } else { + it + } + } + ) + } + + @Keep + @JvmStatic + fun onHideChunkedProgressDialog() { + _saveTransferProgress.value = null + } + // Settings object Settings { @JvmStatic diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 955fb876ddbd..620a66ff5acc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -2,8 +2,6 @@ package org.dolphinemu.dolphinemu.features.netplay.model -import android.app.Application -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -42,6 +40,8 @@ class NetplayViewModel : ViewModel() { private val _maxBuffer = MutableStateFlow(Netplay.Settings.getClientBufferSize()) val maxBuffer = _maxBuffer.asStateFlow() + val saveTransferProgress = Netplay.saveTransferProgress + init { if (!Netplay.isClientConnected()) { _goBack.trySend(Unit) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt new file mode 100644 index 000000000000..e16cbab8ca01 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt @@ -0,0 +1,13 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +data class SaveTransferProgress( + val title: String, + val totalSize: Long, + val playerProgresses: List +) { + data class PlayerProgress( + val playerId: Int, + val name: String, + val progress: Long, + ) +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index 463ad315067f..8a8f86082549 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -52,6 +52,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value, onMaxBufferChanged = viewModel::setMaxBuffer, + saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value, ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 80914e952ce0..40fe1fe5ec41 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar @@ -70,10 +71,12 @@ import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player +import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -87,6 +90,7 @@ fun NetplayScreen( maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, players: List, + saveTransferProgress: SaveTransferProgress?, ) { Scaffold( topBar = { @@ -136,16 +140,31 @@ fun NetplayScreen( LaunchedEffect(Unit) { connectionLost.collect { showConnectionLostDialog = true } } - if (showConnectionLostDialog) { - AlertDialog( - text = { Text(stringResource(R.string.netplay_connection_lost)) }, - confirmButton = { - TextButton(onClick = onBackClicked) { - Text(stringResource(R.string.ok)) - } - }, - onDismissRequest = onBackClicked, - ) + + var dismissSaveTransferProgressDialog by remember { mutableStateOf(false) } + if (saveTransferProgress == null) { + dismissSaveTransferProgressDialog = false + } + + when { + showConnectionLostDialog -> { + AlertDialog( + text = { Text(stringResource(R.string.netplay_connection_lost)) }, + confirmButton = { + TextButton(onClick = onBackClicked) { + Text(stringResource(R.string.ok)) + } + }, + onDismissRequest = onBackClicked, + ) + } + + saveTransferProgress != null && !dismissSaveTransferProgressDialog -> { + SaveTransferProgressDialog( + saveTransferProgress = saveTransferProgress, + onDismiss = { dismissSaveTransferProgressDialog = true }, + ) + } } } } @@ -519,6 +538,68 @@ private fun BufferInput( } } +@Composable +private fun SaveTransferProgressDialog( + saveTransferProgress: SaveTransferProgress, + onDismiss: () -> Unit, +) { + AlertDialog( + title = { Text(saveTransferProgress.title) }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + saveTransferProgress.playerProgresses.forEachIndexed { index, playerProgress -> + SaveTransferProgressRow( + playerProgress = playerProgress, + totalSize = saveTransferProgress.totalSize, + ) + + if (index < saveTransferProgress.playerProgresses.lastIndex) { + HorizontalDivider() + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.netplay_save_transfer_progress_close)) + } + }, + onDismissRequest = onDismiss, + ) +} + +@Composable +private fun SaveTransferProgressRow( + playerProgress: SaveTransferProgress.PlayerProgress, + totalSize: Long, +) { + fun formatMib(bytes: Long) = String.format(Locale.US, "%.2f", bytes / 1024f / 1024f) + val progressFraction = (playerProgress.progress.toFloat() / totalSize).coerceIn(0f, 1f) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + LinearProgressIndicator( + progress = { progressFraction }, + modifier = Modifier.fillMaxWidth(), + ) + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = playerProgress.name, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = "${formatMib(playerProgress.progress)}/${formatMib(totalSize)} MiB", + ) + } + } +} + @Preview @Composable private fun NetplayScreenPreview() { @@ -589,5 +670,22 @@ private fun PreviewNetplayScreen() { hostInputAuthorityEnabled = true, maxBuffer = 10, onMaxBufferChanged = {}, + saveTransferProgress = null, +// saveTransferProgress = SaveTransferProgress( +// title = "Title", +// totalSize = 1024L, +// playerProgresses = listOf( +// SaveTransferProgress.PlayerProgress( +// playerId = 1, +// name = "Player 1", +// progress = 256, +// ), +// SaveTransferProgress.PlayerProgress( +// playerId = 2, +// name = "Player 2", +// progress = 512, +// ), +// ), +// ), ) } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 7b4311c99697..cc1866d4737b 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1009,4 +1009,5 @@ It can efficiently compress both junk data and encrypted Wii data. Mapping Max buffer Netplay connection lost + Close diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 461576f21746..150cb8162eb9 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -40,6 +40,9 @@ static jmethodID s_netplay_on_host_input_authority_changed; static jmethodID s_netplay_on_pad_buffer_changed; static jmethodID s_netplay_on_chat_message_received; static jmethodID s_netplay_update; +static jmethodID s_netplay_on_show_chunked_progress_dialog; +static jmethodID s_netplay_on_set_chunked_progress; +static jmethodID s_netplay_on_hide_chunked_progress_dialog; static jclass s_netplay_player_class; static jmethodID s_netplay_player_constructor; @@ -309,6 +312,21 @@ jmethodID GetNetplayUpdate() return s_netplay_update; } +jmethodID GetNetplayOnShowChunkedProgressDialog() +{ + return s_netplay_on_show_chunked_progress_dialog; +} + +jmethodID GetNetplayOnSetChunkedProgress() +{ + return s_netplay_on_set_chunked_progress; +} + +jmethodID GetNetplayOnHideChunkedProgressDialog() +{ + return s_netplay_on_hide_chunked_progress_dialog; +} + jclass GetNetplayPlayerClass() { return s_netplay_player_class; @@ -741,6 +759,12 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_netplay_on_chat_message_received = env->GetStaticMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); + s_netplay_on_show_chunked_progress_dialog = + env->GetStaticMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); + s_netplay_on_set_chunked_progress = + env->GetStaticMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); + s_netplay_on_hide_chunked_progress_dialog = + env->GetStaticMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index c535fcb9ae11..4041b5060ee7 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -43,6 +43,9 @@ jmethodID GetNetplayOnHostInputAuthorityChanged(); jmethodID GetNetplayOnPadBufferChanged(); jmethodID GetNetplayOnChatMessageReceived(); jmethodID GetNetplayUpdate(); +jmethodID GetNetplayOnShowChunkedProgressDialog(); +jmethodID GetNetplayOnSetChunkedProgress(); +jmethodID GetNetplayOnHideChunkedProgressDialog(); jclass GetNetplayPlayerClass(); jmethodID GetNetplayPlayerConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 21dd77d01971..fa013017dc92 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -186,9 +186,36 @@ void NetPlayUICallbacks::ShowGameDigestDialog(const std::string&) {} void NetPlayUICallbacks::SetGameDigestProgress(int, int) {} void NetPlayUICallbacks::SetGameDigestResult(int, const std::string&) {} void NetPlayUICallbacks::AbortGameDigest() {} -void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string&, u64, std::span) {} -void NetPlayUICallbacks::HideChunkedProgressDialog() {} -void NetPlayUICallbacks::SetChunkedProgress(int, u64) {} + +void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 data_size, + std::span players) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + + jintArray j_players = env->NewIntArray(static_cast(players.size())); + env->SetIntArrayRegion(j_players, 0, static_cast(players.size()), players.data()); + + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnShowChunkedProgressDialog(), + ToJString(env, title), static_cast(data_size), j_players); + env->DeleteLocalRef(j_players); +} + +void NetPlayUICallbacks::HideChunkedProgressDialog() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnHideChunkedProgressDialog()); +} + +void NetPlayUICallbacks::SetChunkedProgress(int pid, u64 progress) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnSetChunkedProgress(), + static_cast(pid), static_cast(progress)); +} + void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) {} } // namespace NetPlay From 183d6d778ca8a87dd5ad1ae3513f7fcfd561369d Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 27 Apr 2026 19:22:31 +0200 Subject: [PATCH 055/270] Use existing settings API instead of custom jni calls --- .../dolphinemu/features/netplay/Netplay.kt | 42 --------- .../features/netplay/model/ConnectionType.kt | 3 + .../netplay/model/NetplaySetupViewModel.kt | 29 +++--- .../netplay/model/NetplayViewModel.kt | 7 +- .../features/settings/model/IntSetting.kt | 9 +- .../features/settings/model/Settings.kt | 1 + .../features/settings/model/StringSetting.kt | 16 +++- Source/Android/jni/NetPlay/Netplay.cpp | 88 ------------------- 8 files changed, 50 insertions(+), 145 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index 882016dfa93b..1cec94f19d24 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -279,47 +278,6 @@ object Netplay { _saveTransferProgress.value = null } - // Settings - object Settings { - @JvmStatic - external fun getNickname(): String - - @JvmStatic - external fun setNickname(nickname: String) - - fun getConnectionType(): ConnectionType = ConnectionType.all - .find { it.configValue == getTraversalChoice() } ?: throw IllegalStateException() - - @JvmStatic - external fun getTraversalChoice(): String - - @JvmStatic - external fun setTraversalChoice(traversalChoice: String) - - @JvmStatic - external fun getAddress(): String - - @JvmStatic - external fun setAddress(address: String) - - @JvmStatic - external fun getHostCode(): String - - @JvmStatic - external fun setHostCode(hostCode: String) - - @JvmStatic - external fun getConnectPort(): Int - - @JvmStatic - external fun setConnectPort(port: Int) - - @JvmStatic - external fun getClientBufferSize(): Int - - @JvmStatic - external fun setClientBufferSize(buffer: Int) - } } private fun Channel.flush() { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt index f6b3f0eca15c..52b4aa10b4eb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt @@ -20,5 +20,8 @@ sealed class ConnectionType( companion object { val all: List get() = listOf(DirectConnection, TraversalServer) + + fun fromString(value: String): ConnectionType = + all.find { it.configValue == value } ?: throw IllegalArgumentException("Invalid connection type: $value") } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index d1bd202c2f5f..2a04e41c35a0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -13,25 +13,30 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.services.GameFileCacheManager class NetplaySetupViewModel : ViewModel() { private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) val connectionRole = _connectionRole.asStateFlow() - private val _nickname = MutableStateFlow(Netplay.Settings.getNickname()) + private val _nickname = MutableStateFlow(StringSetting.NETPLAY_NICKNAME.string) val nickname = _nickname.asStateFlow() - private val _connectionType = MutableStateFlow(Netplay.Settings.getConnectionType()) + private val _connectionType = MutableStateFlow( + ConnectionType.fromString(StringSetting.NETPLAY_TRAVERSAL_CHOICE.string) + ) val connectionType = _connectionType.asStateFlow() - private val _ipAddress = MutableStateFlow(Netplay.Settings.getAddress()) + private val _ipAddress = MutableStateFlow(StringSetting.NETPLAY_ADDRESS.string) val ipAddress = _ipAddress.asStateFlow() - private val _hostCode = MutableStateFlow(Netplay.Settings.getHostCode()) + private val _hostCode = MutableStateFlow(StringSetting.NETPLAY_HOST_CODE.string) val hostCode = _hostCode.asStateFlow() - private val _connectPort = MutableStateFlow(Netplay.Settings.getConnectPort().toString()) + private val _connectPort = MutableStateFlow(IntSetting.NETPLAY_CONNECT_PORT.int.toString()) val connectPort = _connectPort.asStateFlow() private val _showNetplayScreen = Channel(CONFLATED) @@ -52,30 +57,34 @@ class NetplaySetupViewModel : ViewModel() { fun setNickname(nickname: String) { _nickname.value = nickname - Netplay.Settings.setNickname(nickname) + StringSetting.NETPLAY_NICKNAME.setString(NativeConfig.LAYER_BASE, nickname) } fun setConnectionType(connectionType: ConnectionType) { _connectionType.value = connectionType - Netplay.Settings.setTraversalChoice(connectionType.configValue) + StringSetting.NETPLAY_TRAVERSAL_CHOICE.setString( + NativeConfig.LAYER_BASE, connectionType.configValue + ) } fun setIpAddress(ipAddress: String) { if (ipAddress.all { it.isDigit() || it == '.' }) { _ipAddress.value = ipAddress - Netplay.Settings.setAddress(ipAddress) + StringSetting.NETPLAY_ADDRESS.setString(NativeConfig.LAYER_BASE, ipAddress) } } fun setHostCode(hostCode: String) { _hostCode.value = hostCode - Netplay.Settings.setHostCode(hostCode) + StringSetting.NETPLAY_HOST_CODE.setString(NativeConfig.LAYER_BASE, hostCode) } fun setConnectPort(port: String) { if (port.all { it.isDigit() }) { _connectPort.value = port - port.toIntOrNull()?.let { Netplay.Settings.setConnectPort(it) } + port.toIntOrNull()?.let { + IntSetting.NETPLAY_CONNECT_PORT.setInt(NativeConfig.LAYER_BASE, it) + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 620a66ff5acc..66ad26aaecdb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -15,8 +15,9 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig -//TODO save settings class NetplayViewModel : ViewModel() { val launchGame = Netplay.launchGame @@ -37,7 +38,7 @@ class NetplayViewModel : ViewModel() { val hostInputAuthority = Netplay.hostInputAuthorityEnabled .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) - private val _maxBuffer = MutableStateFlow(Netplay.Settings.getClientBufferSize()) + private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) val maxBuffer = _maxBuffer.asStateFlow() val saveTransferProgress = Netplay.saveTransferProgress @@ -59,7 +60,7 @@ class NetplayViewModel : ViewModel() { fun setMaxBuffer(buffer: Int) { _maxBuffer.value = buffer - Netplay.Settings.setClientBufferSize(buffer) + IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, buffer) Netplay.adjustPadBufferSize(buffer) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt index faf9f71ea0da..d8bc8fbba511 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt @@ -138,7 +138,14 @@ enum class IntSetting( WIIMOTE_2_SOURCE(Settings.FILE_WIIMOTE, "Wiimote2", "Source", 0), WIIMOTE_3_SOURCE(Settings.FILE_WIIMOTE, "Wiimote3", "Source", 0), WIIMOTE_4_SOURCE(Settings.FILE_WIIMOTE, "Wiimote4", "Source", 0), - WIIMOTE_BB_SOURCE(Settings.FILE_WIIMOTE, "BalanceBoard", "Source", 0); + WIIMOTE_BB_SOURCE(Settings.FILE_WIIMOTE, "BalanceBoard", "Source", 0), + NETPLAY_CONNECT_PORT(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "ConnectPort", 2626), + NETPLAY_CLIENT_BUFFER_SIZE( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_NETPLAY, + "BufferSizeClient", + 1 + ); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt index 96d40b214ce8..da54d8125844 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt @@ -112,6 +112,7 @@ class Settings : Closeable { const val SECTION_INI_INTERFACE = "Interface" const val SECTION_INI_DSP = "DSP" const val SECTION_INI_GBA = "GBA" + const val SECTION_INI_NETPLAY = "NetPlay" const val SECTION_LOGGER_LOGS = "Logs" const val SECTION_LOGGER_OPTIONS = "Options" const val SECTION_GFX_HARDWARE = "Hardware" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index 512a33ddbb12..d3070a51cae3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -101,7 +101,21 @@ enum class StringSetting( Settings.SECTION_ACHIEVEMENTS, "ApiToken", "" - ); + ), + NETPLAY_TRAVERSAL_CHOICE( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_NETPLAY, + "TraversalChoice", + "direct" + ), + NETPLAY_HOST_CODE( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_NETPLAY, + "HostCode", + "" + ), + NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"), + NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index e8f3deaa1705..62905d203ce5 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -26,94 +26,6 @@ static NetPlay::NetPlayClient* GetPointer(JNIEnv* env) extern "C" { -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getNickname(JNIEnv* env, - jclass) -{ - return ToJString(env, Config::Get(Config::NETPLAY_NICKNAME)); -} - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setNickname(JNIEnv* env, - jclass, - jstring jnickname) -{ - Config::SetBase(Config::NETPLAY_NICKNAME, GetJString(env, jnickname)); -} - -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getTraversalChoice( - JNIEnv* env, jclass) -{ - return ToJString(env, Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE)); -} - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setTraversalChoice( - JNIEnv* env, jclass, jstring jtraversalChoice) -{ - Config::SetBase(Config::NETPLAY_TRAVERSAL_CHOICE, GetJString(env, jtraversalChoice)); -} - -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getAddress(JNIEnv* env, - jclass) -{ - return ToJString(env, Config::Get(Config::NETPLAY_ADDRESS)); -} - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setAddress(JNIEnv* env, - jclass, - jstring jaddress) -{ - Config::SetBase(Config::NETPLAY_ADDRESS, GetJString(env, jaddress)); -} - -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getHostCode(JNIEnv* env, - jclass) -{ - return ToJString(env, Config::Get(Config::NETPLAY_HOST_CODE)); -} - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setHostCode(JNIEnv* env, - jclass, - jstring jhostCode) -{ - Config::SetBase(Config::NETPLAY_HOST_CODE, GetJString(env, jhostCode)); -} - -JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getConnectPort(JNIEnv*, - jclass) -{ - return static_cast(Config::Get(Config::NETPLAY_CONNECT_PORT)); -} - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setConnectPort(JNIEnv*, - jclass, - jint port) -{ - Config::SetBase(Config::NETPLAY_CONNECT_PORT, static_cast(port)); -} - -JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_getClientBufferSize(JNIEnv*, - jclass) -{ - return static_cast(Config::Get(Config::NETPLAY_CLIENT_BUFFER_SIZE)); -} - -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_00024Settings_setClientBufferSize( - JNIEnv*, jclass, jint buffer) -{ - Config::SetBase(Config::NETPLAY_CLIENT_BUFFER_SIZE, static_cast(buffer)); -} - JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass) { From abd324e98d735d836d8131cef91b58f3278bc6fa Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 12 May 2026 11:20:01 -0700 Subject: [PATCH 056/270] Make NetplaySession not a singleton Create a new NetplaySession each time we try to join a netplay game. Hold onto it in NetplayManager so its available to the different activities that need to access it. Close the session when backing out of the netplay UI. Some guardrails in case things go out of sync: creating a session closes the old one if it is still around for some reason, finalizer in NetplaySession to release native resources if not closed explicitly for some reason. Profiling done to ensure all kotlin and native objects are successfully cleared / garbage collected. --- .../dolphinemu/dolphinemu/NativeLibrary.kt | 2 +- .../features/netplay/NetplayManager.kt | 38 +++++ .../netplay/{Netplay.kt => NetplaySession.kt} | 140 ++++++++-------- .../netplay/model/NetplaySetupViewModel.kt | 52 +++++- .../netplay/model/NetplayViewModel.kt | 47 +++--- .../features/netplay/ui/NetplayActivity.kt | 11 +- .../netplay/ui/NetplaySetupActivity.kt | 6 +- .../dolphinemu/fragments/EmulationFragment.kt | 15 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 41 ++--- Source/Android/jni/AndroidCommon/IDCache.h | 2 +- Source/Android/jni/MainAndroid.cpp | 7 +- .../jni/NetPlay/NetPlayUICallbacks.cpp | 152 ++++++++++++++---- .../Android/jni/NetPlay/NetPlayUICallbacks.h | 8 +- Source/Android/jni/NetPlay/Netplay.cpp | 92 +++++++---- 14 files changed, 402 insertions(+), 211 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt rename Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/{Netplay.kt => NetplaySession.kt} (73%) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt index 2e2c295c7de8..b52b7b4fa4ae 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -346,7 +346,7 @@ object NativeLibrary { * Begins emulation for a netplay session, using the BootSessionData provided by the host. */ @JvmStatic - external fun RunNetPlay(paths: Array, riivolution: Boolean) + external fun RunNetPlay(paths: Array, riivolution: Boolean, bootSessionDataPointer: Long) @JvmStatic external fun ChangeDisc(path: String) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt new file mode 100644 index 000000000000..eae44e65be4e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt @@ -0,0 +1,38 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +object NetplayManager { + + private val mutex = Mutex() + + @Volatile + private var closeComplete: CompletableDeferred? = null + + @Volatile + var activeSession: NetplaySession? = null + private set + + suspend fun createSession(): NetplaySession = mutex.withLock { + closeComplete?.await() + + // Sessions should be closed by UI navigation, but just in case. + activeSession?.closeBlocking() + + closeComplete = CompletableDeferred() + + NetplaySession( + onClosed = { + activeSession = null + closeComplete?.complete(Unit) + } + ).also { + activeSession = it + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt similarity index 73% rename from Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt rename to Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 1cec94f19d24..f47d9ab198c8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -6,7 +6,6 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow @@ -29,14 +28,21 @@ import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress -object Netplay { - @Keep +class NetplaySession( + private val onClosed: (NetplaySession) -> Unit, +) { + + private var netPlayUICallbacksPointer: Long = nativeCreateUICallbacks() + private var netPlayClientPointer: Long = 0 - @Keep private var bootSessionDataPointer: Long = 0 - private var sessionScope: CoroutineScope? = null + private val sessionScope = CoroutineScope(SupervisorJob()) + + @Volatile + var isClosed = false + private set val isLaunching: Boolean get() = bootSessionDataPointer != 0L @@ -93,97 +99,96 @@ object Netplay { val saveTransferProgress = _saveTransferProgress.asStateFlow() suspend fun join(): Boolean = withContext(Dispatchers.IO) { - val scope = createSessionScope() + if (isClosed) throw IllegalStateException("Cannot join a closed session") - // Gather all messages that should appear in the chat window. mergeMessages() .runningFold(emptyList()) { acc, msg -> listOf(msg) + acc } .onEach { _messages.tryEmit(it) } - .launchIn(scope) + .launchIn(sessionScope) - netPlayClientPointer = Join() - val isConnected = netPlayClientPointer != 0L && isClientConnected() + netPlayClientPointer = nativeJoin() - if (!isActive) { - releaseNetplayClient() + if (netPlayClientPointer == 0L || !isActive) { + closeBlocking() return@withContext false } - if (isConnected) { - return@withContext true + true + } + + fun sendMessage(message: String) = nativeSendMessage(message) + + fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + + fun consumeBootSessionData(): Long { + return bootSessionDataPointer.also { + bootSessionDataPointer = 0 } + } - releaseNetplayClient() - false + suspend fun close() = withContext(Dispatchers.IO) { + closeBlocking() } - suspend fun quit() = withContext(Dispatchers.IO) { - releaseNetplayClient() + @Synchronized + fun closeBlocking() { + if (isClosed) return + isClosed = true + sessionScope.cancel() + releaseNativeResources() + onClosed(this) } - @OptIn(ExperimentalCoroutinesApi::class) - private fun releaseNetplayClient() { - sessionScope?.cancel() - sessionScope = null + protected fun finalize() { + releaseNativeResources() + } - if (bootSessionDataPointer != 0L) { - ReleaseBootSessionData() + private fun mergeMessages(): Flow = merge( + chatMessages.map { NetplayMessage.Chat(it) }, + game.map { NetplayMessage.GameChanged(it) }, + hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, + padBuffer.map { NetplayMessage.BufferChanged(it) }, + ) + + private fun releaseNativeResources() { + val currentBootSessionDataPointer = bootSessionDataPointer + if (currentBootSessionDataPointer != 0L) { bootSessionDataPointer = 0 + nativeReleaseBootSessionData(currentBootSessionDataPointer) } - if (netPlayClientPointer != 0L) { - ReleaseNetplayClient() + val currentNetPlayClientPointer = netPlayClientPointer + if (currentNetPlayClientPointer != 0L) { netPlayClientPointer = 0 + nativeReleaseClient(currentNetPlayClientPointer) } - _launchGame.flush() - _stopGame.flush() - _connectionErrors.flush() - _players.resetReplayCache() - _messages.resetReplayCache() - _chatMessages.resetReplayCache() - _game.resetReplayCache() - _hostInputAuthorityEnabled.resetReplayCache() - _padBuffer.resetReplayCache() - _saveTransferProgress.value = null - } - - private fun createSessionScope(): CoroutineScope { - sessionScope?.cancel() - return CoroutineScope(SupervisorJob() + Dispatchers.IO).also { - sessionScope = it + val currentNetPlayUICallbacksPointer = netPlayUICallbacksPointer + if (currentNetPlayUICallbacksPointer != 0L) { + netPlayUICallbacksPointer = 0 + nativeReleaseUICallbacks(currentNetPlayUICallbacksPointer) } } - @JvmStatic - private external fun Join(): Long + // JNI methods - @JvmStatic - external fun isClientConnected(): Boolean + private external fun nativeCreateUICallbacks(): Long - @JvmStatic - external fun sendMessage(message: String) + private external fun nativeJoin(): Long - @JvmStatic - external fun adjustPadBufferSize(buffer: Int) + private external fun nativeSendMessage(message: String) - @JvmStatic - private external fun ReleaseBootSessionData() + private external fun nativeAdjustPadBufferSize(buffer: Int) - @JvmStatic - private external fun ReleaseNetplayClient() + private external fun nativeReleaseUICallbacks(pointer: Long) - private fun mergeMessages(): Flow = merge( - chatMessages.map { NetplayMessage.Chat(it) }, - game.map { NetplayMessage.GameChanged(it) }, - hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, - padBuffer.map { NetplayMessage.BufferChanged(it) }, - ) + private external fun nativeReleaseClient(pointer: Long) + + private external fun nativeReleaseBootSessionData(pointer: Long) // NetPlayUI callbacks @Keep - @JvmStatic fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { this.bootSessionDataPointer = bootSessionDataPointer _stopGame.flush() @@ -191,57 +196,47 @@ object Netplay { } @Keep - @JvmStatic fun onStopGame() { _stopGame.trySend(Unit) } @Keep - @JvmStatic fun onConnectionLost() { _connectionLost.trySend(Unit) } @Keep - @JvmStatic fun onConnectionError(message: String) { _connectionErrors.trySend(message) } @Keep - @JvmStatic fun onUpdate(players: Array) { _players.tryEmit(players.toList()) } @Keep - @JvmStatic fun onChatMessageReceived(message: String) { _chatMessages.tryEmit(message) } @Keep - @JvmStatic fun onHostInputAuthorityChanged(enabled: Boolean) { _hostInputAuthorityEnabled.tryEmit(enabled) } @Keep - @JvmStatic fun onGameChanged(game: String) { _game.tryEmit(game) } @Keep - @JvmStatic fun onPadBufferChanged(buffer: Int) { - // Only for remote pad buffer settings. Ignore local max buffer changes. if (_hostInputAuthorityEnabled.replayCache.firstOrNull() == true) return _padBuffer.tryEmit(buffer) } @Keep - @JvmStatic fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) { val players = _players.replayCache.firstOrNull() _saveTransferProgress.value = SaveTransferProgress( @@ -258,7 +253,6 @@ object Netplay { } @Keep - @JvmStatic fun onSetChunkedProgress(playerId: Int, progress: Long) { val current = _saveTransferProgress.value _saveTransferProgress.value = current?.copy( @@ -273,11 +267,9 @@ object Netplay { } @Keep - @JvmStatic fun onHideChunkedProgressDialog() { _saveTransferProgress.value = null } - } private fun Channel.flush() { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 2a04e41c35a0..a9b34d3147d0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -3,22 +3,31 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.services.GameFileCacheManager -class NetplaySetupViewModel : ViewModel() { +class NetplaySetupViewModel( + private val netplayManager: NetplayManager, +) : ViewModel() { + private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) val connectionRole = _connectionRole.asStateFlow() @@ -45,7 +54,8 @@ class NetplaySetupViewModel : ViewModel() { private val _connecting = MutableStateFlow(false) val connecting = _connecting.asStateFlow() - val errors = Netplay.connectionErrors + private val _errors = MutableSharedFlow(extraBufferCapacity = 8) + val errors = _errors.asSharedFlow() init { GameFileCacheManager.startLoad() @@ -89,16 +99,42 @@ class NetplaySetupViewModel : ViewModel() { } fun connect() { + if (_connecting.value) return + _connecting.value = true viewModelScope.launch { - GameFileCacheManager.isLoading().asFlow().first { it == false } - - if (Netplay.join()) { - _showNetplayScreen.trySend(Unit) + var errorForwarding: Job? = null + + try { + GameFileCacheManager.isLoading().asFlow().first { it == false } + + val session = netplayManager.createSession() + errorForwarding = session.connectionErrors + .onEach { _errors.emit(it) } + .launchIn(this) + + if (session.join()) { + _showNetplayScreen.trySend(Unit) + } + } finally { + errorForwarding?.cancel() + _connecting.value = false } + } + } + + override fun onCleared() { + super.onCleared() + // There should not be an active session at this point but in case one was created + // but launching the Netplay screen failed, close it. + netplayManager.activeSession?.closeBlocking() + } - _connecting.value = false + class Factory(private val netplayManager: NetplayManager) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return NetplaySetupViewModel(netplayManager) as T } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 66ad26aaecdb..96f9d7328b54 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -3,51 +3,43 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.NetplaySession import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig -class NetplayViewModel : ViewModel() { - val launchGame = Netplay.launchGame +class NetplayViewModel( + private val netplaySession: NetplaySession, +) : ViewModel() { - private val _goBack = Channel(CONFLATED) - val goBack = _goBack.receiveAsFlow() + val launchGame = netplaySession.launchGame - val connectionLost = Netplay.connectionLost + val connectionLost = netplaySession.connectionLost - val players = Netplay.players + val players = netplaySession.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - val messages = Netplay.messages + val messages = netplaySession.messages .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - val game = Netplay.game + val game = netplaySession.game .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") - val hostInputAuthority = Netplay.hostInputAuthorityEnabled + val hostInputAuthority = netplaySession.hostInputAuthorityEnabled .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) val maxBuffer = _maxBuffer.asStateFlow() - val saveTransferProgress = Netplay.saveTransferProgress - - init { - if (!Netplay.isClientConnected()) { - _goBack.trySend(Unit) - } - } + val saveTransferProgress = netplaySession.saveTransferProgress fun sendMessage(message: String) { val trimmedMessage = message.trim() @@ -55,20 +47,29 @@ class NetplayViewModel : ViewModel() { return } - Netplay.sendMessage(trimmedMessage) + netplaySession.sendMessage(trimmedMessage) } fun setMaxBuffer(buffer: Int) { _maxBuffer.value = buffer IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, buffer) - Netplay.adjustPadBufferSize(buffer) + netplaySession.adjustPadBufferSize(buffer) } @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() + // Closing the netplay session is a bit slow for the main thread so launch in + // GlobalScope and allow the activity and view model to finish immediately. GlobalScope.launch { - Netplay.quit() + netplaySession.close() + } + } + + class Factory(private val session: NetplaySession) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return NetplayViewModel(session) as T } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index 8a8f86082549..f2acf50107e8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @@ -29,11 +30,13 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { enableEdgeToEdge() super.onCreate(savedInstanceState) - val viewModel = ViewModelProvider(this)[NetplayViewModel::class.java] + val session = NetplayManager.activeSession + if (session == null) { + finish() + return + } - viewModel.goBack - .onEach { finish() } - .launchIn(lifecycleScope) + val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session))[NetplayViewModel::class.java] viewModel.launchGame .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index 8058b3ad96e8..145cf7d6352b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @@ -28,7 +29,10 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { enableEdgeToEdge() super.onCreate(savedInstanceState) - val viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] + val viewModel = ViewModelProvider( + this, + NetplaySetupViewModel.Factory(NetplayManager) + )[NetplaySetupViewModel::class.java] viewModel.showNetplayScreen .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index 49331d8a4cab..c989cdc78063 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding -import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.overlay.InputOverlay @@ -211,7 +211,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // Don't load temporary saves when launching Netplay, this path can trigger // when a game starts due to orientation changes caused by a mismatch in menu // vs emulation activity orientations. - if (loadPreviousTemporaryState && !Netplay.isLaunching) { + val netplaySession = NetplayManager.activeSession + if (loadPreviousTemporaryState && netplaySession?.isLaunching != true) { Log.debug("[EmulationFragment] Starting emulation thread from previous state.") val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" @@ -221,16 +222,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (launchSystemMenu) { Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") NativeLibrary.RunSystemMenu() - } else if (Netplay.isLaunching) { + } else if (netplaySession?.isLaunching == true) { Log.debug("[EmulationFragment] Starting emulation thread for Netplay.") val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" } lifecycleScope.launch { - Netplay.stopGame.first() + netplaySession.stopGame.first() stopEmulation() } - NativeLibrary.RunNetPlay(paths, riivolution) + NativeLibrary.RunNetPlay( + paths, + riivolution, + netplaySession.consumeBootSessionData() + ) } else { Log.debug("[EmulationFragment] Starting emulation thread.") val paths = requireNotNull(gamePaths) { diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 150cb8162eb9..2bee9e213870 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -29,8 +29,8 @@ static jclass s_game_file_cache_manager_class; static jfieldID s_game_file_cache_manager_instance; static jclass s_netplay_class; +static jfieldID s_net_play_ui_callbacks_pointer; static jfieldID s_net_play_client_pointer; -static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_stop_game; static jmethodID s_netplay_on_connection_lost; @@ -257,14 +257,14 @@ jclass GetNetplayClass() return s_netplay_class; } -jfieldID GetNetPlayClientPointer() +jfieldID GetNetPlayUICallbacksPointer() { - return s_net_play_client_pointer; + return s_net_play_ui_callbacks_pointer; } -jfieldID GetNetplayBootSessionDataPointer() +jfieldID GetNetPlayClientPointer() { - return s_netplay_boot_session_data_pointer; + return s_net_play_client_pointer; } jmethodID GetNetplayOnBootGame() @@ -742,29 +742,30 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->DeleteLocalRef(game_file_cache_manager_class); const jclass netplay_class = - env->FindClass("org/dolphinemu/dolphinemu/features/netplay/Netplay"); + env->FindClass("org/dolphinemu/dolphinemu/features/netplay/NetplaySession"); s_netplay_class = reinterpret_cast(env->NewGlobalRef(netplay_class)); - s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J"); - s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); - s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); - s_netplay_on_stop_game = env->GetStaticMethodID(netplay_class, "onStopGame", "()V"); - s_netplay_on_connection_lost = env->GetStaticMethodID(netplay_class, "onConnectionLost", "()V"); - s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); + s_net_play_ui_callbacks_pointer = + env->GetFieldID(netplay_class, "netPlayUICallbacksPointer", "J"); + s_net_play_client_pointer = env->GetFieldID(netplay_class, "netPlayClientPointer", "J"); + s_netplay_on_boot_game = env->GetMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); + s_netplay_on_stop_game = env->GetMethodID(netplay_class, "onStopGame", "()V"); + s_netplay_on_connection_lost = env->GetMethodID(netplay_class, "onConnectionLost", "()V"); + s_netplay_on_connection_error = env->GetMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = - env->GetStaticMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); + env->GetMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); s_netplay_on_host_input_authority_changed = - env->GetStaticMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); + env->GetMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); s_netplay_on_pad_buffer_changed = - env->GetStaticMethodID(netplay_class, "onPadBufferChanged", "(I)V"); + env->GetMethodID(netplay_class, "onPadBufferChanged", "(I)V"); s_netplay_on_chat_message_received = - env->GetStaticMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); - s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); + env->GetMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); + s_netplay_update = env->GetMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); s_netplay_on_show_chunked_progress_dialog = - env->GetStaticMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); + env->GetMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); s_netplay_on_set_chunked_progress = - env->GetStaticMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); + env->GetMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); s_netplay_on_hide_chunked_progress_dialog = - env->GetStaticMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); + env->GetMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 4041b5060ee7..c5f0a4557357 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -32,8 +32,8 @@ jclass GetGameFileCacheManagerClass(); jfieldID GetGameFileCacheManagerInstance(); jclass GetNetplayClass(); +jfieldID GetNetPlayUICallbacksPointer(); jfieldID GetNetPlayClientPointer(); -jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnStopGame(); jmethodID GetNetplayOnConnectionLost(); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 13471a6cd0fc..0d29ad390861 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -618,10 +618,10 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_Run___3Ljava_lang_String_2ZLjava_la } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunNetPlay( - JNIEnv* env, jclass, jobjectArray jPaths, jboolean jRiivolution) + JNIEnv* env, jclass, jobjectArray jPaths, jboolean jRiivolution, jlong jBootSessionData) { - auto boot_session_data = std::unique_ptr(reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer()))); + auto boot_session_data = std::unique_ptr( + reinterpret_cast(jBootSessionData)); if (!boot_session_data) { env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetDisplayToastMsg(), @@ -629,7 +629,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunNetPlay( env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetFinishEmulationActivity()); return; } - env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0); Run(env, JStringArrayToVector(env, jPaths), jRiivolution, std::move(*boot_session_data)); } diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index fa013017dc92..1f9e701edb74 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -10,29 +10,54 @@ namespace NetPlay { -NetPlayUICallbacks::NetPlayUICallbacks(std::vector> games) - : m_games(std::move(games)) +NetPlayUICallbacks::NetPlayUICallbacks(jobject netplay_session, + std::vector> games) + : m_netplay_session(IDCache::GetEnvForThread()->NewWeakGlobalRef(netplay_session)), + m_games(std::move(games)) { m_state_changed_hook = Core::AddOnStateChangedCallback([this](Core::State state) { if ((state == Core::State::Uninitialized || state == Core::State::Stopping) && !m_got_stop_request) { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + auto* client = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); if (client) client->RequestStopGame(); + + env->DeleteLocalRef(netplay_session); } }); } -NetPlayUICallbacks::~NetPlayUICallbacks() = default; +NetPlayUICallbacks::~NetPlayUICallbacks() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->DeleteWeakGlobalRef(m_netplay_session); +} + +jobject NetPlayUICallbacks::GetNetplaySessionLocalRef(JNIEnv* env) const +{ + return env->NewLocalRef(m_netplay_session); +} + +void NetPlayUICallbacks::BootGame(const std::string& filename, + std::unique_ptr boot_session_data) +{ + m_got_stop_request = false; + + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; -void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr boot_session_data) { - m_got_stop_request = false; - JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnBootGame(), - ToJString(env, filename), reinterpret_cast(boot_session_data.release())); + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnBootGame(), ToJString(env, filename), + reinterpret_cast(boot_session_data.release())); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::StopGame() @@ -41,8 +66,14 @@ void NetPlayUICallbacks::StopGame() return; m_got_stop_request = true; + JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnStopGame()); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnStopGame()); + env->DeleteLocalRef(netplay_session); } bool NetPlayUICallbacks::IsHosting() const { return false; } @@ -50,10 +81,17 @@ bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + auto* client = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); if (!client) + { + env->DeleteLocalRef(netplay_session); return; + } const std::vector players = client->GetPlayers(); @@ -77,16 +115,21 @@ void NetPlayUICallbacks::Update() env->DeleteLocalRef(player_obj); } - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayUpdate(), player_array); + env->CallVoidMethod(netplay_session, IDCache::GetNetplayUpdate(), player_array); env->DeleteLocalRef(player_array); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::AppendChat(const std::string& message) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnChatMessageReceived(), - ToJString(env, message)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnChatMessageReceived(), + ToJString(env, message)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, @@ -96,8 +139,13 @@ void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_ide m_current_game_name = netplay_name; JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnGameChanged(), - ToJString(env, netplay_name)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnGameChanged(), + ToJString(env, netplay_name)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} @@ -105,13 +153,19 @@ void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} void NetPlayUICallbacks::OnMsgStartGame() { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + auto* client = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); if (client) { if (const auto game = FindGameFile(m_current_game_identifier)) client->StartGame(game->GetFilePath()); } + + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnMsgStopGame() {} @@ -122,16 +176,25 @@ void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnPadBufferChanged(), - static_cast(buffer)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnPadBufferChanged(), + static_cast(buffer)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnHostInputAuthorityChanged(), - static_cast(enabled)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHostInputAuthorityChanged(), + static_cast(enabled)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} @@ -139,14 +202,24 @@ void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} void NetPlayUICallbacks::OnConnectionLost() { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionLost()); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnConnectionLost()); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnConnectionError(const std::string& message) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionError(), - ToJString(env, message)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnConnectionError(), + ToJString(env, message)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {} @@ -191,29 +264,40 @@ void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 std::span players) { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; jintArray j_players = env->NewIntArray(static_cast(players.size())); env->SetIntArrayRegion(j_players, 0, static_cast(players.size()), players.data()); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnShowChunkedProgressDialog(), - ToJString(env, title), static_cast(data_size), j_players); + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnShowChunkedProgressDialog(), + ToJString(env, title), static_cast(data_size), j_players); env->DeleteLocalRef(j_players); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::HideChunkedProgressDialog() { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnHideChunkedProgressDialog()); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHideChunkedProgressDialog()); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::SetChunkedProgress(int pid, u64 progress) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnSetChunkedProgress(), - static_cast(pid), static_cast(progress)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetChunkedProgress(), + static_cast(pid), static_cast(progress)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) {} diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index 10c1b1ac2249..57eec55230f3 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -5,6 +5,8 @@ #include #include +#include + #include "Common/HookableEvent.h" #include "Core/NetPlayClient.h" #include "UICommon/GameFile.h" @@ -13,7 +15,8 @@ namespace NetPlay { class NetPlayUICallbacks : public NetPlay::NetPlayUI { public: - NetPlayUICallbacks(std::vector> games); + NetPlayUICallbacks(jobject netplay_session, + std::vector> games); ~NetPlayUICallbacks() override; void BootGame(const std::string& filename, @@ -59,6 +62,9 @@ class NetPlayUICallbacks : public NetPlay::NetPlayUI { void SetHostWiiSyncData(std::vector titles, std::string redirect_folder) override; private: + jobject GetNetplaySessionLocalRef(JNIEnv* env) const; + + jweak m_netplay_session; std::vector> m_games; NetPlay::SyncIdentifier m_current_game_identifier; std::string m_current_game_name; diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 62905d203ce5..61c2e77b9f57 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -18,51 +18,41 @@ #include "jni/AndroidCommon/IDCache.h" #include "jni/NetPlay/NetPlayUICallbacks.h" -static NetPlay::NetPlayClient* GetPointer(JNIEnv* env) +static NetPlay::NetPlayUICallbacks* GetUICallbacksPointer(JNIEnv* env, jobject obj) { - return reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); } -extern "C" { - -JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass) +static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) { - return static_cast(GetPointer(env)->IsConnected()); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayClientPointer())); } +extern "C" { + JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_sendMessage(JNIEnv* env, jclass, - jstring jmessage) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage(JNIEnv* env, jobject obj, + jstring jmessage) { - if (auto* client = GetPointer(env)) + if (auto* client = GetClientPointer(env, obj)) client->SendChatMessage(GetJString(env, jmessage)); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_adjustPadBufferSize(JNIEnv* env, jclass, - jint buffer) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustPadBufferSize(JNIEnv* env, + jobject obj, + jint buffer) { - if (auto* client = GetPointer(env)) + if (auto* client = GetClientPointer(env, obj)) client->AdjustPadBufferSize(static_cast(buffer)); } JNIEXPORT jlong JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeCreateUICallbacks(JNIEnv* env, + jobject obj) { - const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); - const bool is_traversal = traversal_choice == "traversal"; - - std::string host_ip; - host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : - Config::Get(Config::NETPLAY_ADDRESS); - - const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); - const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); - const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); - const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); - jobject jgame_file_cache = env->GetStaticObjectField( IDCache::GetGameFileCacheManagerClass(), IDCache::GetGameFileCacheManagerInstance()); auto* game_file_cache = reinterpret_cast( @@ -72,26 +62,58 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass game_file_cache->ForEach( [&games](const std::shared_ptr& game) { games.push_back(game); }); + return reinterpret_cast(new NetPlay::NetPlayUICallbacks(obj, std::move(games))); +} + +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv* env, jobject obj) +{ + auto* ui = GetUICallbacksPointer(env, obj); + + const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); + const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); + const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); + + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + const bool is_traversal = traversal_choice == "traversal"; + const std::string host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : + Config::Get(Config::NETPLAY_ADDRESS); + const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + auto* client = new NetPlay::NetPlayClient( - host_ip, host_port, new NetPlay::NetPlayUICallbacks(std::move(games)), nickname, + host_ip, host_port, ui, nickname, NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port}); + if (!client->IsConnected()) + { + delete client; + return 0; + } + return reinterpret_cast(client); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseBootSessionData(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, + jobject, + jlong pointer) +{ + delete reinterpret_cast(pointer); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseBootSessionData(JNIEnv*, + jobject, + jlong pointer) { - auto* data = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer())); - delete data; - env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0); + delete reinterpret_cast(pointer); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseNetplayClient(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseClient(JNIEnv*, jobject, + jlong pointer) { - delete GetPointer(env); + delete reinterpret_cast(pointer); } } // extern "C" From 117d1d71acc6b85b6c67396bd2222fbee7f0ff05 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 29 Apr 2026 14:31:25 +0200 Subject: [PATCH 057/270] Handle desync messages Show them in the chat window and also in a toast during game play. --- .../dolphinemu/features/netplay/NetplaySession.kt | 12 ++++++++++++ .../features/netplay/model/NetplayMessage.kt | 5 +++++ .../dolphinemu/fragments/EmulationFragment.kt | 7 +++++++ Source/Android/app/src/main/res/values/strings.xml | 1 + Source/Android/jni/AndroidCommon/IDCache.cpp | 8 ++++++++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp | 12 +++++++++++- 7 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index f47d9ab198c8..fbeba1510ceb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -95,6 +95,12 @@ class NetplaySession( ) val padBuffer = _padBuffer.asSharedFlow() + private val _desyncMessages = MutableSharedFlow( + extraBufferCapacity = 32, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val desyncMessages = _desyncMessages.asSharedFlow() + private val _saveTransferProgress = MutableStateFlow(null) val saveTransferProgress = _saveTransferProgress.asStateFlow() @@ -148,6 +154,7 @@ class NetplaySession( game.map { NetplayMessage.GameChanged(it) }, hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, padBuffer.map { NetplayMessage.BufferChanged(it) }, + desyncMessages, ) private fun releaseNativeResources() { @@ -236,6 +243,11 @@ class NetplaySession( _padBuffer.tryEmit(buffer) } + @Keep + fun onDesync(frame: Int, player: String) { + _desyncMessages.tryEmit(NetplayMessage.Desync(player, frame)) + } + @Keep fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) { val players = _players.replayCache.firstOrNull() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt index 41ab8997bdb9..b8291c094bd4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt @@ -26,4 +26,9 @@ sealed class NetplayMessage { override fun message(context: Context) = context.getString(R.string.netplay_message_buffer_changed, buffer) } + + class Desync(private val player: String, private val frame: Int) : NetplayMessage() { + override fun message(context: Context) = + context.getString(R.string.netplay_message_desync, player, frame) + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index c989cdc78063..a005995bdba7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -9,9 +9,12 @@ import android.view.LayoutInflater import android.view.SurfaceHolder import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity @@ -231,6 +234,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { netplaySession.stopGame.first() stopEmulation() } + netplaySession + .desyncMessages + .onEach { Toast.makeText(requireContext(), it.message(requireContext()), Toast.LENGTH_SHORT).show() } + .launchIn(lifecycleScope) NativeLibrary.RunNetPlay( paths, riivolution, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index cc1866d4737b..4e9c977faaef 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1002,6 +1002,7 @@ It can efficiently compress both junk data and encrypted Wii data. Game changed to %1$s Buffer size changed to %1$d "Host input authority %1$s" + Possible desync detected: %1$s might have desynced at frame %2$d Game Players Name diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 2bee9e213870..caf305fc9bcf 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -43,6 +43,7 @@ static jmethodID s_netplay_update; static jmethodID s_netplay_on_show_chunked_progress_dialog; static jmethodID s_netplay_on_set_chunked_progress; static jmethodID s_netplay_on_hide_chunked_progress_dialog; +static jmethodID s_netplay_on_desync; static jclass s_netplay_player_class; static jmethodID s_netplay_player_constructor; @@ -327,6 +328,11 @@ jmethodID GetNetplayOnHideChunkedProgressDialog() return s_netplay_on_hide_chunked_progress_dialog; } +jmethodID GetNetplayOnDesync() +{ + return s_netplay_on_desync; +} + jclass GetNetplayPlayerClass() { return s_netplay_player_class; @@ -766,6 +772,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); s_netplay_on_hide_chunked_progress_dialog = env->GetMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); + s_netplay_on_desync = + env->GetMethodID(netplay_class, "onDesync", "(ILjava/lang/String;)V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index c5f0a4557357..ae86740f09fd 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -46,6 +46,7 @@ jmethodID GetNetplayUpdate(); jmethodID GetNetplayOnShowChunkedProgressDialog(); jmethodID GetNetplayOnSetChunkedProgress(); jmethodID GetNetplayOnHideChunkedProgressDialog(); +jmethodID GetNetplayOnDesync(); jclass GetNetplayPlayerClass(); jmethodID GetNetplayPlayerConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 1f9e701edb74..c411119ab9c5 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -197,7 +197,17 @@ void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) env->DeleteLocalRef(netplay_session); } -void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} +void NetPlayUICallbacks::OnDesync(u32 frame, const std::string& player) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnDesync(), + static_cast(frame), ToJString(env, player)); + env->DeleteLocalRef(netplay_session); +} void NetPlayUICallbacks::OnConnectionLost() { From acbf9e155f8e0397e6bfa2f3d00bad0e5f5ecbcf Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Thu, 30 Apr 2026 10:16:20 +0200 Subject: [PATCH 058/270] Game digest progress dialog We just about get away with using a StateFlow in NetplaySession since the host sends AbortGameDigest when closing their own dialog. Without that it would be harder for the UI to distinguish between subsequent dialogs. If that wasn't the case then NetplaySession might need to expose the individual progress and result updates and have the view model assemble it into the overall GameDigestProgress. --- .../features/netplay/NetplaySession.kt | 58 +++++++++++ .../netplay/model/GameDigestProgress.kt | 14 +++ .../netplay/model/NetplayViewModel.kt | 2 + .../features/netplay/ui/NetplayActivity.kt | 1 + .../features/netplay/ui/NetplayScreen.kt | 97 ++++++++++++++++++- .../app/src/main/res/values/strings.xml | 3 + Source/Android/jni/AndroidCommon/IDCache.cpp | 32 ++++++ Source/Android/jni/AndroidCommon/IDCache.h | 4 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 51 +++++++++- 9 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index fbeba1510ceb..e0531a64776a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -104,6 +105,9 @@ class NetplaySession( private val _saveTransferProgress = MutableStateFlow(null) val saveTransferProgress = _saveTransferProgress.asStateFlow() + private val _gameDigestProgress = MutableStateFlow(null) + val gameDigestProgress = _gameDigestProgress.asStateFlow() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { if (isClosed) throw IllegalStateException("Cannot join a closed session") @@ -282,6 +286,60 @@ class NetplaySession( fun onHideChunkedProgressDialog() { _saveTransferProgress.value = null } + + @Keep + fun onShowGameDigestDialog(title: String) { + val players = _players.replayCache.firstOrNull() + _gameDigestProgress.value = GameDigestProgress( + title = title, + playerProgresses = players?.map { player -> + GameDigestProgress.PlayerProgress( + playerId = player.pid, + name = player.name, + progress = 0, + result = null, + ) + } ?: emptyList(), + matches = null, + ) + } + + @Keep + fun onSetGameDigestProgress(playerId: Int, progress: Int) { + val current = _gameDigestProgress.value ?: return + _gameDigestProgress.value = current.copy( + playerProgresses = current.playerProgresses.map { + if (it.playerId == playerId) it.copy(progress = progress) else it + } + ) + } + + @Keep + fun onSetGameDigestResult(playerId: Int, result: String) { + val current = _gameDigestProgress.value ?: return + val updated = current.copy( + playerProgresses = current.playerProgresses.map { + if (it.playerId == playerId) it.copy(result = result) else it + } + ) + val finished = updated.playerProgresses.all { it.result != null } + _gameDigestProgress.value = if (finished) { + val results = updated.playerProgresses.map { it.result } + updated.copy(matches = results.distinct().size == 1) + } else { + updated + } + } + + /** + * Hosts send this when they dismiss their dialog even in a successful scenario. Ensuring + * that the value is cleared before a new game digest is started. Without this, StateFlow + * would not be a good choice. + */ + @Keep + fun onAbortGameDigest() { + _gameDigestProgress.value = null + } } private fun Channel.flush() { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt new file mode 100644 index 000000000000..57a9cae78b23 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt @@ -0,0 +1,14 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +data class GameDigestProgress( + val title: String, + val playerProgresses: List, + val matches: Boolean?, +) { + data class PlayerProgress( + val playerId: Int, + val name: String, + val progress: Int, + val result: String?, + ) +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 96f9d7328b54..d0f86ed3f68e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -41,6 +41,8 @@ class NetplayViewModel( val saveTransferProgress = netplaySession.saveTransferProgress + val gameDigestProgress = netplaySession.gameDigestProgress + fun sendMessage(message: String) { val trimmedMessage = message.trim() if (trimmedMessage.isEmpty()) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index f2acf50107e8..d99c4da57a92 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -56,6 +56,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { maxBuffer = viewModel.maxBuffer.collectAsState().value, onMaxBufferChanged = viewModel::setMaxBuffer, saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value, + gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value, ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 40fe1fe5ec41..71595ddb4334 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -69,6 +70,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -91,6 +93,7 @@ fun NetplayScreen( onMaxBufferChanged: (Int) -> Unit, players: List, saveTransferProgress: SaveTransferProgress?, + gameDigestProgress: GameDigestProgress?, ) { Scaffold( topBar = { @@ -136,16 +139,21 @@ fun NetplayScreen( ) } - var showConnectionLostDialog by remember { mutableStateOf(false) } + var showConnectionLostDialog by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { connectionLost.collect { showConnectionLostDialog = true } } - var dismissSaveTransferProgressDialog by remember { mutableStateOf(false) } + var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) } if (saveTransferProgress == null) { dismissSaveTransferProgressDialog = false } + var dismissGameDigestDialog by rememberSaveable { mutableStateOf(false) } + if (gameDigestProgress == null) { + dismissGameDigestDialog = false + } + when { showConnectionLostDialog -> { AlertDialog( @@ -165,6 +173,13 @@ fun NetplayScreen( onDismiss = { dismissSaveTransferProgressDialog = true }, ) } + + gameDigestProgress != null && !dismissGameDigestDialog -> { + GameDigestProgressDialog( + gameDigestProgress = gameDigestProgress, + onDismiss = { dismissGameDigestDialog = true }, + ) + } } } } @@ -548,6 +563,7 @@ private fun SaveTransferProgressDialog( text = { Column( verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.verticalScroll(rememberScrollState()), ) { saveTransferProgress.playerProgresses.forEachIndexed { index, playerProgress -> SaveTransferProgressRow( @@ -600,6 +616,82 @@ private fun SaveTransferProgressRow( } } +@Composable +private fun GameDigestProgressDialog( + gameDigestProgress: GameDigestProgress, + onDismiss: () -> Unit, +) { + AlertDialog( + title = { Text(gameDigestProgress.title) }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + gameDigestProgress.playerProgresses.forEachIndexed { index, playerProgress -> + GameDigestPlayerRow(playerProgress) + if (index < gameDigestProgress.playerProgresses.lastIndex) { + HorizontalDivider() + } + } + if (gameDigestProgress.matches != null) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource( + if (gameDigestProgress.matches) { + R.string.netplay_game_digest_match + } else { + R.string.netplay_game_digest_mismatch + } + ), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + ) + } + } + }, + confirmButton = { + if (gameDigestProgress.matches != null) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.netplay_game_digest_close)) + } + } + }, + onDismissRequest = { onDismiss() }, + ) +} + +@Composable +private fun GameDigestPlayerRow( + playerProgress: GameDigestProgress.PlayerProgress, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + LinearProgressIndicator( + progress = { playerProgress.progress / 100f }, + modifier = Modifier.fillMaxWidth(), + ) + Row( + modifier = Modifier.fillMaxWidth(), + ) { + if (playerProgress.result == null) { + Text( + text = playerProgress.name, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${playerProgress.progress}%", + ) + } else { + Text( + text = "${playerProgress.name}:\u00A0${playerProgress.result}", + ) + } + } + } +} + @Preview @Composable private fun NetplayScreenPreview() { @@ -671,6 +763,7 @@ private fun PreviewNetplayScreen() { maxBuffer = 10, onMaxBufferChanged = {}, saveTransferProgress = null, + gameDigestProgress = null, // saveTransferProgress = SaveTransferProgress( // title = "Title", // totalSize = 1024L, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 4e9c977faaef..ef103432aa05 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1011,4 +1011,7 @@ It can efficiently compress both junk data and encrypted Wii data. Max buffer Netplay connection lost Close + The hashes match + The hashes do not match + Close diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index caf305fc9bcf..91f75990788a 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -44,6 +44,10 @@ static jmethodID s_netplay_on_show_chunked_progress_dialog; static jmethodID s_netplay_on_set_chunked_progress; static jmethodID s_netplay_on_hide_chunked_progress_dialog; static jmethodID s_netplay_on_desync; +static jmethodID s_netplay_on_show_game_digest_dialog; +static jmethodID s_netplay_on_set_game_digest_progress; +static jmethodID s_netplay_on_set_game_digest_result; +static jmethodID s_netplay_on_abort_game_digest; static jclass s_netplay_player_class; static jmethodID s_netplay_player_constructor; @@ -333,6 +337,26 @@ jmethodID GetNetplayOnDesync() return s_netplay_on_desync; } +jmethodID GetNetplayOnShowGameDigestDialog() +{ + return s_netplay_on_show_game_digest_dialog; +} + +jmethodID GetNetplayOnSetGameDigestProgress() +{ + return s_netplay_on_set_game_digest_progress; +} + +jmethodID GetNetplayOnSetGameDigestResult() +{ + return s_netplay_on_set_game_digest_result; +} + +jmethodID GetNetplayOnAbortGameDigest() +{ + return s_netplay_on_abort_game_digest; +} + jclass GetNetplayPlayerClass() { return s_netplay_player_class; @@ -774,6 +798,14 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); s_netplay_on_desync = env->GetMethodID(netplay_class, "onDesync", "(ILjava/lang/String;)V"); + s_netplay_on_show_game_digest_dialog = + env->GetMethodID(netplay_class, "onShowGameDigestDialog", "(Ljava/lang/String;)V"); + s_netplay_on_set_game_digest_progress = + env->GetMethodID(netplay_class, "onSetGameDigestProgress", "(II)V"); + s_netplay_on_set_game_digest_result = + env->GetMethodID(netplay_class, "onSetGameDigestResult", "(ILjava/lang/String;)V"); + s_netplay_on_abort_game_digest = + env->GetMethodID(netplay_class, "onAbortGameDigest", "()V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index ae86740f09fd..f6103809083e 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -47,6 +47,10 @@ jmethodID GetNetplayOnShowChunkedProgressDialog(); jmethodID GetNetplayOnSetChunkedProgress(); jmethodID GetNetplayOnHideChunkedProgressDialog(); jmethodID GetNetplayOnDesync(); +jmethodID GetNetplayOnShowGameDigestDialog(); +jmethodID GetNetplayOnSetGameDigestProgress(); +jmethodID GetNetplayOnSetGameDigestResult(); +jmethodID GetNetplayOnAbortGameDigest(); jclass GetNetplayPlayerClass(); jmethodID GetNetplayPlayerConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index c411119ab9c5..018d5556ddda 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -265,10 +265,53 @@ NetPlayUICallbacks::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, } std::string NetPlayUICallbacks::FindGBARomPath(const std::array&, std::string_view, int) { return {}; } -void NetPlayUICallbacks::ShowGameDigestDialog(const std::string&) {} -void NetPlayUICallbacks::SetGameDigestProgress(int, int) {} -void NetPlayUICallbacks::SetGameDigestResult(int, const std::string&) {} -void NetPlayUICallbacks::AbortGameDigest() {} + +void NetPlayUICallbacks::ShowGameDigestDialog(const std::string& title) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnShowGameDigestDialog(), + ToJString(env, title)); + env->DeleteLocalRef(netplay_session); +} + +void NetPlayUICallbacks::SetGameDigestProgress(int pid, int progress) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetGameDigestProgress(), + static_cast(pid), static_cast(progress)); + env->DeleteLocalRef(netplay_session); +} + +void NetPlayUICallbacks::SetGameDigestResult(int pid, const std::string& result) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetGameDigestResult(), + static_cast(pid), ToJString(env, result)); + env->DeleteLocalRef(netplay_session); +} + +void NetPlayUICallbacks::AbortGameDigest() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnAbortGameDigest()); + env->DeleteLocalRef(netplay_session); +} void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 data_size, std::span players) From f9baecd5eda524a0227ed4697b6853fb1a446b18 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sat, 2 May 2026 15:21:44 +0200 Subject: [PATCH 059/270] Implement OnMsgPowerButton --- Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 018d5556ddda..2ea0b814a734 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "UICommon/GameFile.h" +#include "UICommon/UICommon.h" #include "NetPlayUICallbacks.h" #include "Core/Boot/Boot.h" #include "Core/Core.h" +#include "Core/System.h" #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" @@ -169,7 +171,13 @@ void NetPlayUICallbacks::OnMsgStartGame() } void NetPlayUICallbacks::OnMsgStopGame() {} -void NetPlayUICallbacks::OnMsgPowerButton() {} + +void NetPlayUICallbacks::OnMsgPowerButton() +{ + if (Core::IsRunning(Core::System::GetInstance())) + UICommon::TriggerSTMPowerEvent(); +} + void NetPlayUICallbacks::OnPlayerConnect(const std::string&) {} void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} From b63ccacdfa7355989e0ca971b9b06ef1dcfb3bbb Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 6 May 2026 10:43:26 +0200 Subject: [PATCH 060/270] Hosting UI --- .../netplay/model/NetplaySetupViewModel.kt | 25 ++++++ .../features/netplay/ui/NetplayActivity.kt | 2 + .../features/netplay/ui/NetplayScreen.kt | 50 ++++++++--- .../netplay/ui/NetplaySetupActivity.kt | 5 ++ .../features/netplay/ui/NetplaySetupScreen.kt | 89 ++++++++++++++++++- .../features/settings/model/BooleanSetting.kt | 3 +- .../features/settings/model/IntSetting.kt | 1 + .../dolphinemu/ui/theme/DolphinTheme.kt | 1 + .../app/src/main/res/values/strings.xml | 2 + 9 files changed, 163 insertions(+), 15 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index a9b34d3147d0..d51ae477acab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.NetplayManager +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting @@ -48,6 +49,12 @@ class NetplaySetupViewModel( private val _connectPort = MutableStateFlow(IntSetting.NETPLAY_CONNECT_PORT.int.toString()) val connectPort = _connectPort.asStateFlow() + private val _hostPort = MutableStateFlow(IntSetting.NETPLAY_HOST_PORT.int.toString()) + val hostPort = _hostPort.asStateFlow() + + private val _useUpnp = MutableStateFlow(BooleanSetting.NETPLAY_USE_UPNP.boolean) + val useUpnp = _useUpnp.asStateFlow() + private val _showNetplayScreen = Channel(CONFLATED) val showNetplayScreen = _showNetplayScreen.receiveAsFlow() @@ -98,6 +105,24 @@ class NetplaySetupViewModel( } } + fun setHostPort(port: String) { + if (port.all { it.isDigit() }) { + _hostPort.value = port + port.toIntOrNull()?.let { + IntSetting.NETPLAY_HOST_PORT.setInt(NativeConfig.LAYER_BASE, it) + } + } + } + + fun setUseUpnp(useUpnp: Boolean) { + _useUpnp.value = useUpnp + BooleanSetting.NETPLAY_USE_UPNP.setBoolean(NativeConfig.LAYER_BASE, useUpnp) + } + + fun host() { + + } + fun connect() { if (_connecting.value) return diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index d99c4da57a92..c01ec2a76c30 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -51,6 +51,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, + isHosting = false, + onStartGame = {}, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 71595ddb4334..45bd89efabf1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -84,10 +85,12 @@ import java.util.Locale @Composable fun NetplayScreen( onBackClicked: () -> Unit, + isHosting: Boolean, connectionLost: Flow, messages: List, onSendMessage: (String) -> Unit, game: String, + onStartGame: () -> Unit, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, @@ -109,6 +112,13 @@ fun NetplayScreen( }, ) }, + floatingActionButton = { + if (isHosting) { + ExtendedFloatingActionButton(onClick = onStartGame) { + Text(stringResource(R.string.netplay_start)) + } + } + }, ) { innerPadding -> val modifier = Modifier .fillMaxSize() @@ -117,6 +127,7 @@ fun NetplayScreen( if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( + isHosting = isHosting, messages = messages, onSendMessage = onSendMessage, game = game, @@ -128,6 +139,7 @@ fun NetplayScreen( ) } else { PortraitContent( + isHosting = isHosting, messages = messages, onSendMessage = onSendMessage, game = game, @@ -186,6 +198,7 @@ fun NetplayScreen( @Composable private fun PortraitContent( + isHosting: Boolean, messages: List, onSendMessage: (String) -> Unit, game: String, @@ -197,13 +210,14 @@ private fun PortraitContent( ) { Column( modifier = modifier + .verticalScroll(rememberScrollState()) ) { Chat( messages = messages, onSendMessage = onSendMessage, modifier = Modifier .fillMaxWidth() - .fillMaxHeight(0.3f) + .height(200.dp) .padding(horizontal = DolphinTheme.scaffoldPadding) ) @@ -216,14 +230,18 @@ private fun PortraitContent( maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, modifier = Modifier - .weight(1f) .padding(horizontal = DolphinTheme.scaffoldPadding), ) + + if (isHosting) { + Spacer(modifier = Modifier.height(DolphinTheme.fabClearancePadding)) + } } } @Composable private fun LandscapeContent( + isHosting: Boolean, messages: List, onSendMessage: (String) -> Unit, game: String, @@ -245,16 +263,25 @@ private fun LandscapeContent( .padding(horizontal = DolphinTheme.scaffoldPadding) ) - PlayersAndSettings( - game = game, - players = players, - hostInputAuthorityEnabled = hostInputAuthorityEnabled, - maxBuffer = maxBuffer, - onMaxBufferChanged = onMaxBufferChanged, + Column( modifier = Modifier .weight(1f) - .padding(horizontal = DolphinTheme.scaffoldPadding) - ) + .verticalScroll(rememberScrollState()) + ) { + PlayersAndSettings( + game = game, + players = players, + hostInputAuthorityEnabled = hostInputAuthorityEnabled, + maxBuffer = maxBuffer, + onMaxBufferChanged = onMaxBufferChanged, + modifier = Modifier + .padding(horizontal = DolphinTheme.scaffoldPadding) + ) + + if (isHosting) { + Spacer(modifier = Modifier.height(DolphinTheme.fabClearancePadding)) + } + } } } @@ -269,7 +296,6 @@ private fun PlayersAndSettings( ) { Column( modifier = modifier - .verticalScroll(rememberScrollState()) ) { OutlinedTextField( value = game, @@ -759,6 +785,8 @@ private fun PreviewNetplayScreen() { }, onSendMessage = {}, game = "Game name", + isHosting = true, + onStartGame = {}, hostInputAuthorityEnabled = true, maxBuffer = 10, onMaxBufferChanged = {}, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index 145cf7d6352b..a82dbff15a60 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -57,6 +57,11 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { onConnectPortChanged = viewModel::setConnectPort, hostCode = viewModel.hostCode.collectAsState().value, onHostCodeChanged = viewModel::setHostCode, + hostPort = viewModel.hostPort.collectAsState().value, + onHostPortChanged = viewModel::setHostPort, + useUpnp = viewModel.useUpnp.collectAsState().value, + onUseUpnpChanged = viewModel::setUseUpnp, + onHostClicked = viewModel::host, onConnectClicked = viewModel::connect, ) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt index bcc11ef4bb12..939a39524d12 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -3,10 +3,12 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -16,6 +18,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -27,6 +30,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SecondaryTabRow @@ -39,6 +43,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType @@ -74,6 +79,11 @@ fun NetplaySetupScreen( onConnectPortChanged: (String) -> Unit, hostCode: String, onHostCodeChanged: (String) -> Unit, + hostPort: String, + onHostPortChanged: (String) -> Unit, + useUpnp: Boolean, + onUseUpnpChanged: (Boolean) -> Unit, + onHostClicked: () -> Unit, onConnectClicked: () -> Unit, ) { Scaffold( @@ -92,7 +102,10 @@ fun NetplaySetupScreen( }, floatingActionButton = { ExtendedFloatingActionButton( - onClick = onConnectClicked, + onClick = when(connectionRole) { + ConnectionRole.Host -> onHostClicked + ConnectionRole.Connect -> onConnectClicked + }, ) { if (connecting) { CircularProgressIndicator( @@ -134,6 +147,7 @@ fun NetplaySetupScreen( .consumeWindowInsets(innerPadding) .verticalScroll(rememberScrollState()) .padding(innerPadding) + .padding(bottom = DolphinTheme.fabClearancePadding) ) { SecondaryTabRow(selectedTabIndex = ConnectionRole.all.indexOf(connectionRole)) { ConnectionRole.all.forEach { role -> @@ -162,6 +176,10 @@ fun NetplaySetupScreen( onHostCodeChanged = onHostCodeChanged, connectPort = connectPort, onConnectPortChanged = onConnectPortChanged, + hostPort = hostPort, + onHostPortChanged = onHostPortChanged, + useUpnp = useUpnp, + onUseUpnpChanged = onUseUpnpChanged, connectionRole = connectionRole, ) } @@ -181,6 +199,10 @@ private fun NetplaySetupContent( onHostCodeChanged: (String) -> Unit, connectPort: String, onConnectPortChanged: (String) -> Unit, + hostPort: String, + onHostPortChanged: (String) -> Unit, + useUpnp: Boolean, + onUseUpnpChanged: (Boolean) -> Unit, connectionRole: ConnectionRole, ) { OutlinedTextField( @@ -211,7 +233,13 @@ private fun NetplaySetupContent( onPortChanged = onConnectPortChanged, ) - ConnectionRole.Host -> {} + ConnectionRole.Host -> HostMenu( + connectionType = connectionType, + hostPort = hostPort, + onHostPortChanged = onHostPortChanged, + useUpnp = useUpnp, + onUseUpnpChanged = onUseUpnpChanged, + ) } } @@ -304,6 +332,56 @@ fun ConnectMenu( } } +@Composable +private fun HostMenu( + connectionType: ConnectionType, + hostPort: String, + onHostPortChanged: (String) -> Unit, + useUpnp: Boolean, + onUseUpnpChanged: (Boolean) -> Unit, +) { + if (connectionType == ConnectionType.DirectConnection) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + OutlinedTextField( + value = hostPort, + onValueChange = onHostPortChanged, + label = { Text(stringResource(R.string.netplay_host_port_label)) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + singleLine = true, + modifier = Modifier + .weight(1f) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + OutlinedButton( + onClick = { onUseUpnpChanged(!useUpnp) }, + shape = MaterialTheme.shapes.extraSmall, + modifier = Modifier + .height(64.dp) + .padding(top = 8.dp) + ) { + Text( + text = stringResource(R.string.netplay_use_upnp), + ) + Spacer(modifier = Modifier.width(12.dp)) + Checkbox( + checked = useUpnp, + onCheckedChange = null, + modifier = Modifier.size(24.dp), + ) + } + } + + MenuSpacer() + } +} + @Preview @Composable private fun NetplaySetupScreenPreview() { @@ -312,7 +390,7 @@ private fun NetplaySetupScreenPreview() { onBackClicked = {}, connecting = false, errors = emptyFlow(), - connectionRole = ConnectionRole.Connect, + connectionRole = ConnectionRole.Host, onConnectionRoleChanged = {}, nickname = "Preview nickname", onNicknameChanged = {}, @@ -324,6 +402,11 @@ private fun NetplaySetupScreenPreview() { onConnectPortChanged = {}, hostCode = "", onHostCodeChanged = {}, + hostPort = "2626", + onHostPortChanged = {}, + useUpnp = false, + onUseUpnpChanged = {}, + onHostClicked = {}, onConnectClicked = {}, ) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index cd0ea42d943a..4658814ec16d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -933,7 +933,8 @@ enum class BooleanSetting( Settings.SECTION_ACHIEVEMENTS, "ProgressEnabled", false - ); + ), + NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt index d8bc8fbba511..85f265442b16 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt @@ -140,6 +140,7 @@ enum class IntSetting( WIIMOTE_4_SOURCE(Settings.FILE_WIIMOTE, "Wiimote4", "Source", 0), WIIMOTE_BB_SOURCE(Settings.FILE_WIIMOTE, "BalanceBoard", "Source", 0), NETPLAY_CONNECT_PORT(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "ConnectPort", 2626), + NETPLAY_HOST_PORT(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "HostPort", 2626), NETPLAY_CLIENT_BUFFER_SIZE( Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index cf768e6dc086..d25a34586275 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -36,6 +36,7 @@ import com.google.android.material.R as MaterialR object DolphinTheme { val scaffoldPadding = 16.dp + val fabClearancePadding = 80.dp } @Composable diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index ef103432aa05..9c89d3aed749 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1014,4 +1014,6 @@ It can efficiently compress both junk data and encrypted Wii data. The hashes match The hashes do not match Close + Port + Forward port (UPnP) From 77f08364db41de05e06d02128c3107c2d45df105 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 12 May 2026 11:20:46 -0700 Subject: [PATCH 061/270] Create NetPlayServer and start game --- .../features/netplay/NetplaySession.kt | 31 +++++++ .../netplay/model/NetplaySetupViewModel.kt | 13 +-- .../netplay/model/NetplayViewModel.kt | 6 ++ .../features/netplay/ui/NetplayActivity.kt | 4 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 7 ++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 2 + Source/Android/jni/NetPlay/Netplay.cpp | 87 +++++++++++++++++-- 8 files changed, 137 insertions(+), 14 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index e0531a64776a..1ec663a1a53e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -37,6 +37,8 @@ class NetplaySession( private var netPlayClientPointer: Long = 0 + private var netPlayServerPointer: Long = 0 + private var bootSessionDataPointer: Long = 0 private val sessionScope = CoroutineScope(SupervisorJob()) @@ -44,6 +46,9 @@ class NetplaySession( @Volatile var isClosed = false private set + + val isHosting: Boolean + get() = netPlayServerPointer != 0L val isLaunching: Boolean get() = bootSessionDataPointer != 0L @@ -126,10 +131,24 @@ class NetplaySession( true } + suspend fun host(): Boolean = withContext(Dispatchers.IO) { + if (isClosed) throw IllegalStateException("Cannot host a closed session") + + netPlayServerPointer = nativeHost() + if (netPlayServerPointer == 0L || !isActive) { + closeBlocking() + return@withContext false + } + + join() + } + fun sendMessage(message: String) = nativeSendMessage(message) fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + fun startGame() = nativeStartGame() + fun consumeBootSessionData(): Long { return bootSessionDataPointer.also { bootSessionDataPointer = 0 @@ -174,6 +193,12 @@ class NetplaySession( nativeReleaseClient(currentNetPlayClientPointer) } + val currentNetPlayServerPointer = netPlayServerPointer + if (currentNetPlayServerPointer != 0L) { + netPlayServerPointer = 0 + nativeReleaseServer(currentNetPlayServerPointer) + } + val currentNetPlayUICallbacksPointer = netPlayUICallbacksPointer if (currentNetPlayUICallbacksPointer != 0L) { netPlayUICallbacksPointer = 0 @@ -187,6 +212,8 @@ class NetplaySession( private external fun nativeJoin(): Long + private external fun nativeHost(): Long + private external fun nativeSendMessage(message: String) private external fun nativeAdjustPadBufferSize(buffer: Int) @@ -195,8 +222,12 @@ class NetplaySession( private external fun nativeReleaseClient(pointer: Long) + private external fun nativeReleaseServer(pointer: Long) + private external fun nativeReleaseBootSessionData(pointer: Long) + private external fun nativeStartGame() + // NetPlayUI callbacks @Keep diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index d51ae477acab..9c667245e5f7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -119,11 +119,9 @@ class NetplaySetupViewModel( BooleanSetting.NETPLAY_USE_UPNP.setBoolean(NativeConfig.LAYER_BASE, useUpnp) } - fun host() { + fun host() = connect(host = true) - } - - fun connect() { + fun connect(host: Boolean = false) { if (_connecting.value) return _connecting.value = true @@ -139,7 +137,12 @@ class NetplaySetupViewModel( .onEach { _errors.emit(it) } .launchIn(this) - if (session.join()) { + val success = if (host) { + session.host() + } else { + session.join() + } + if (success) { _showNetplayScreen.trySend(Unit) } } finally { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index d0f86ed3f68e..e7518bdf433a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -22,6 +22,8 @@ class NetplayViewModel( val launchGame = netplaySession.launchGame + val isHosting = netplaySession.isHosting + val connectionLost = netplaySession.connectionLost val players = netplaySession.players @@ -43,6 +45,10 @@ class NetplayViewModel( val gameDigestProgress = netplaySession.gameDigestProgress + fun startGame() { + netplaySession.startGame() + } + fun sendMessage(message: String) { val trimmedMessage = message.trim() if (trimmedMessage.isEmpty()) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index c01ec2a76c30..ce1f7dec19c4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -51,8 +51,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, - isHosting = false, - onStartGame = {}, + isHosting = viewModel.isHosting, + onStartGame = viewModel::startGame, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value, diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 91f75990788a..41d70554714c 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -31,6 +31,7 @@ static jfieldID s_game_file_cache_manager_instance; static jclass s_netplay_class; static jfieldID s_net_play_ui_callbacks_pointer; static jfieldID s_net_play_client_pointer; +static jfieldID s_net_play_server_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_stop_game; static jmethodID s_netplay_on_connection_lost; @@ -272,6 +273,11 @@ jfieldID GetNetPlayClientPointer() return s_net_play_client_pointer; } +jfieldID GetNetPlayServerPointer() +{ + return s_net_play_server_pointer; +} + jmethodID GetNetplayOnBootGame() { return s_netplay_on_boot_game; @@ -777,6 +783,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_net_play_ui_callbacks_pointer = env->GetFieldID(netplay_class, "netPlayUICallbacksPointer", "J"); s_net_play_client_pointer = env->GetFieldID(netplay_class, "netPlayClientPointer", "J"); + s_net_play_server_pointer = env->GetFieldID(netplay_class, "netPlayServerPointer", "J"); s_netplay_on_boot_game = env->GetMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_stop_game = env->GetMethodID(netplay_class, "onStopGame", "()V"); s_netplay_on_connection_lost = env->GetMethodID(netplay_class, "onConnectionLost", "()V"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index f6103809083e..51a5ae02bc2e 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -34,6 +34,7 @@ jfieldID GetGameFileCacheManagerInstance(); jclass GetNetplayClass(); jfieldID GetNetPlayUICallbacksPointer(); jfieldID GetNetPlayClientPointer(); +jfieldID GetNetPlayServerPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnStopGame(); jmethodID GetNetplayOnConnectionLost(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 2ea0b814a734..ecbf168fb5fe 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -78,6 +78,8 @@ void NetPlayUICallbacks::StopGame() env->DeleteLocalRef(netplay_session); } +// Only used by Qt UI code, never by the C++ core. On Android, hosting state +// is tracked in Kotlin (NetplaySession.isHosting). bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 61c2e77b9f57..1dd511954ff6 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -11,6 +11,7 @@ #include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" +#include "Core/NetPlayServer.h" #include "UICommon/GameFile.h" #include "UICommon/GameFileCache.h" @@ -20,8 +21,8 @@ static NetPlay::NetPlayUICallbacks* GetUICallbacksPointer(JNIEnv* env, jobject obj) { - return reinterpret_cast( - env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); } static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) @@ -30,6 +31,12 @@ static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) env->GetLongField(obj, IDCache::GetNetPlayClientPointer())); } +static NetPlay::NetPlayServer* GetServerPointer(JNIEnv* env, jobject obj) +{ + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayServerPointer())); +} + extern "C" { JNIEXPORT void JNICALL @@ -74,11 +81,25 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); - const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); - const bool is_traversal = traversal_choice == "traversal"; - const std::string host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : - Config::Get(Config::NETPLAY_ADDRESS); - const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + std::string host_ip; + u16 host_port; + bool is_traversal; + + // When hosting, join our own server on localhost + if (auto* server = GetServerPointer(env, obj)) + { + host_ip = "127.0.0.1"; + host_port = server->GetPort(); + is_traversal = false; + } + else + { + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + is_traversal = traversal_choice == "traversal"; + host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : + Config::Get(Config::NETPLAY_ADDRESS); + host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + } auto* client = new NetPlay::NetPlayClient( host_ip, host_port, ui, nickname, @@ -93,6 +114,51 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv return reinterpret_cast(client); } +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv* env, jobject obj) +{ + auto* ui = GetUICallbacksPointer(env, obj); + + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + const bool is_traversal = traversal_choice == "traversal"; + const bool use_upnp = Config::Get(Config::NETPLAY_USE_UPNP); + const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); + const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); + const u16 traversal_port_alt = Config::Get(Config::NETPLAY_TRAVERSAL_PORT_ALT); + + const u16 host_port = is_traversal ? Config::Get(Config::NETPLAY_LISTEN_PORT) + : Config::Get(Config::NETPLAY_HOST_PORT); + + auto* server = new NetPlay::NetPlayServer( + host_port, use_upnp, ui, + NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port, traversal_port_alt}); + + if (!server->is_connected) + { + delete server; + return 0; + } + + const std::string network_mode = Config::Get(Config::NETPLAY_NETWORK_MODE); + const bool host_input_authority = + network_mode == "hostinputauthority" || network_mode == "golf"; + server->SetHostInputAuthority(host_input_authority); + server->AdjustPadBufferSize(Config::Get(Config::NETPLAY_BUFFER_SIZE)); + + return reinterpret_cast(server); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, + jobject obj) +{ + auto* server = GetServerPointer(env, obj); + if (!server) + return; + + server->RequestStartGame(); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, jobject, @@ -116,4 +182,11 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseClie delete reinterpret_cast(pointer); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseServer(JNIEnv*, jobject, + jlong pointer) +{ + delete reinterpret_cast(pointer); +} + } // extern "C" From 68e9fd6cde8168f591881b8d94b3f6ac4ee55d15 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sat, 2 May 2026 22:57:35 +0200 Subject: [PATCH 062/270] Send own messages to chat A player's own messages don't come back via the server. --- .../dolphinemu/features/netplay/NetplaySession.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 1ec663a1a53e..ba8a0a321ddb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -28,6 +28,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting class NetplaySession( private val onClosed: (NetplaySession) -> Unit, @@ -53,6 +54,8 @@ class NetplaySession( val isLaunching: Boolean get() = bootSessionDataPointer != 0L + val nickName by lazy { StringSetting.NETPLAY_NICKNAME.string } + private val _launchGame = Channel(Channel.CONFLATED) val launchGame = _launchGame.receiveAsFlow() @@ -143,7 +146,10 @@ class NetplaySession( join() } - fun sendMessage(message: String) = nativeSendMessage(message) + fun sendMessage(message: String) { + _chatMessages.tryEmit( "$nickName: $message") + nativeSendMessage(message) + } fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) From 6655672ee3d15cceb9fdb73f4b16df9553fefeb1 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sun, 3 May 2026 10:05:08 +0200 Subject: [PATCH 063/270] Add ability to choose game when hosting Also fix bottom sheets so they survive rotation --- Source/Android/app/build.gradle.kts | 3 + .../features/netplay/NetplaySession.kt | 5 + .../netplay/model/NetplayViewModel.kt | 34 +++ .../features/netplay/ui/NetplayActivity.kt | 3 + .../features/netplay/ui/NetplayScreen.kt | 213 +++++++++++++++++- .../features/settings/model/StringSetting.kt | 3 +- .../dolphinemu/dolphinemu/utils/CoilUtils.kt | 2 +- Source/Android/gradle/libs.versions.toml | 3 + Source/Android/jni/NetPlay/Netplay.cpp | 15 ++ 9 files changed, 267 insertions(+), 14 deletions(-) diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 912906d2d88d..27f76b22a2d3 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -141,6 +141,7 @@ dependencies { implementation(libs.androidx.profileinstaller) // Kotlin extensions for lifecycle components + implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -151,6 +152,7 @@ dependencies { // For loading game covers from disk and GameTDB implementation(libs.coil) + implementation(libs.coil.compose) // For loading custom GPU drivers implementation(libs.kotlinx.serialization.json) @@ -164,6 +166,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material.icons) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.ui) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling.preview) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index ba8a0a321ddb..3ec1dea43f78 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress +import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -153,6 +154,8 @@ class NetplaySession( fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile) + fun startGame() = nativeStartGame() fun consumeBootSessionData(): Long { @@ -232,6 +235,8 @@ class NetplaySession( private external fun nativeReleaseBootSessionData(pointer: Long) + private external fun nativeChangeGame(gameFile: GameFile) + private external fun nativeStartGame() // NetPlayUI callbacks diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index e7518bdf433a..50edaba597cf 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -4,17 +4,22 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.NetplaySession import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.services.GameFileCacheManager class NetplayViewModel( private val netplaySession: NetplaySession, @@ -41,10 +46,24 @@ class NetplayViewModel( private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) val maxBuffer = _maxBuffer.asStateFlow() + val gameFiles = GameFileCacheManager.getGameFiles().asFlow() + .map { it.toList() } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + GameFileCacheManager.getGameFiles().value?.toList() ?: emptyList() + ) + val saveTransferProgress = netplaySession.saveTransferProgress val gameDigestProgress = netplaySession.gameDigestProgress + init { + if (netplaySession.isHosting) { + setInitialGame() + } + } + fun startGame() { netplaySession.startGame() } @@ -64,6 +83,21 @@ class NetplayViewModel( netplaySession.adjustPadBufferSize(buffer) } + fun changeGame(gameFile: GameFile) { + StringSetting.NETPLAY_GAME.setString(NativeConfig.LAYER_BASE, gameFile.getGameId()) + netplaySession.changeGame(gameFile) + } + + private fun setInitialGame() { + val game = gameFiles.value + .find { it.getGameId() == StringSetting.NETPLAY_GAME.string } + ?: gameFiles.value.firstOrNull() + + if (game != null) { + changeGame(game) + } + } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index ce1f7dec19c4..55888dde6de2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle @@ -53,6 +54,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { game = viewModel.game.collectAsState().value, isHosting = viewModel.isHosting, onStartGame = viewModel::startGame, + onGameSelected = viewModel::changeGame, + gameFiles = viewModel.gameFiles.collectAsState().value, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 45bd89efabf1..86cbfe512932 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -3,12 +3,14 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.res.Configuration +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -19,6 +21,9 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -30,6 +35,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider @@ -42,9 +48,10 @@ import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -55,6 +62,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -66,8 +75,11 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R @@ -75,10 +87,12 @@ import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress +import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme +import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -91,6 +105,8 @@ fun NetplayScreen( onSendMessage: (String) -> Unit, game: String, onStartGame: () -> Unit, + onGameSelected: (GameFile) -> Unit, + gameFiles: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, @@ -125,12 +141,21 @@ fun NetplayScreen( .consumeWindowInsets(innerPadding) .padding(innerPadding) + var showChat by rememberSaveable { mutableStateOf(false) } + var showGamePicker by rememberSaveable { mutableStateOf(false) } + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( isHosting = isHosting, messages = messages, onSendMessage = onSendMessage, + showChat = showChat, + onShowChatChanged = { showChat = it }, game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, @@ -142,7 +167,13 @@ fun NetplayScreen( isHosting = isHosting, messages = messages, onSendMessage = onSendMessage, + showChat = showChat, + onShowChatChanged = { showChat = it }, game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, @@ -201,7 +232,13 @@ private fun PortraitContent( isHosting: Boolean, messages: List, onSendMessage: (String) -> Unit, + showChat: Boolean, + onShowChatChanged: (Boolean) -> Unit, game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, @@ -215,6 +252,8 @@ private fun PortraitContent( Chat( messages = messages, onSendMessage = onSendMessage, + showBottomSheet = showChat, + onShowBottomSheetChanged = onShowChatChanged, modifier = Modifier .fillMaxWidth() .height(200.dp) @@ -225,10 +264,15 @@ private fun PortraitContent( PlayersAndSettings( game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + isHosting = isHosting, modifier = Modifier .padding(horizontal = DolphinTheme.scaffoldPadding), ) @@ -244,7 +288,13 @@ private fun LandscapeContent( isHosting: Boolean, messages: List, onSendMessage: (String) -> Unit, + showChat: Boolean, + onShowChatChanged: (Boolean) -> Unit, game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, @@ -257,6 +307,8 @@ private fun LandscapeContent( Chat( messages = messages, onSendMessage = onSendMessage, + showBottomSheet = showChat, + onShowBottomSheetChanged = onShowChatChanged, modifier = Modifier .weight(1f) .fillMaxHeight() @@ -270,10 +322,15 @@ private fun LandscapeContent( ) { PlayersAndSettings( game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + isHosting = isHosting, modifier = Modifier .padding(horizontal = DolphinTheme.scaffoldPadding) ) @@ -288,22 +345,27 @@ private fun LandscapeContent( @Composable private fun PlayersAndSettings( game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, + isHosting: Boolean, modifier: Modifier = Modifier, ) { Column( modifier = modifier ) { - OutlinedTextField( - value = game, - onValueChange = {}, - label = { Text(stringResource(R.string.netplay_game_label)) }, - readOnly = true, - modifier = Modifier - .fillMaxWidth() + GamePicker( + game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = onShowGamePickerChanged, + isHosting = isHosting, ) MenuSpacer() @@ -345,6 +407,8 @@ private fun PlayersAndSettings( private fun Chat( messages: List, onSendMessage: (String) -> Unit, + showBottomSheet: Boolean, + onShowBottomSheetChanged: (Boolean) -> Unit, modifier: Modifier, ) { val context = LocalContext.current @@ -361,12 +425,18 @@ private fun Chat( draftMessage = "" } - var showBottomSheet by remember { mutableStateOf(false) } - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val density = LocalDensity.current + val bottomSheetState = remember { + SheetState( + skipPartiallyExpanded = true, + density = density, + initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden, + ) + } if (showBottomSheet) { ModalBottomSheet( - onDismissRequest = { showBottomSheet = false }, + onDismissRequest = { onShowBottomSheetChanged(false) }, sheetState = bottomSheetState, modifier = Modifier .statusBarsPadding() @@ -407,7 +477,7 @@ private fun Chat( } OutlinedBox( - onClick = { showBottomSheet = true }, + onClick = { onShowBottomSheetChanged(true) }, label = { Text(stringResource(R.string.netplay_chat_label)) }, modifier = modifier ) { @@ -422,6 +492,123 @@ private fun Chat( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun GamePicker( + game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, + isHosting: Boolean, +) { + val density = LocalDensity.current + val bottomSheetState = remember { + SheetState( + skipPartiallyExpanded = true, + density = density, + initialValue = if (showGamePicker) SheetValue.Expanded else SheetValue.Hidden, + ) + } + + if (showGamePicker) { + ModalBottomSheet( + onDismissRequest = { onShowGamePickerChanged(false) }, + sheetState = bottomSheetState, + modifier = Modifier.statusBarsPadding() + ) { + GameList( + gameFiles = gameFiles, + onGameSelected = { gameFile -> + onGameSelected(gameFile) + onShowGamePickerChanged(false) + }, + contentPadding = PaddingValues( + start = DolphinTheme.scaffoldPadding, + end = DolphinTheme.scaffoldPadding, + bottom = 16.dp + ), + ) + } + } + + Box( + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = game, + onValueChange = {}, + label = { Text(stringResource(R.string.netplay_game_label)) }, + readOnly = true, + modifier = Modifier.fillMaxWidth() + ) + if (isHosting) { + Box( + modifier = Modifier + .matchParentSize() + .padding(top = 8.dp) + .clip(MaterialTheme.shapes.extraSmall) + .clickable { onShowGamePickerChanged(true) } + ) + } + } +} + +@Composable +private fun GameList( + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + contentPadding: PaddingValues = PaddingValues(), +) { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 120.dp), + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(gameFiles, key = { it.getPath() }) { gameFile -> + GameGridItem( + gameFile = gameFile, + onClick = { onGameSelected(gameFile) }, + ) + } + } +} + +@Composable +private fun GameGridItem( + gameFile: GameFile, + onClick: () -> Unit, +) { + Card( + onClick = onClick, + ) { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(gameFile) + .error(R.drawable.no_banner) + .build(), + contentDescription = gameFile.getTitle(), + contentScale = ContentScale.Crop, + imageLoader = CoilUtils.imageLoader, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(0.7f) + ) + Text( + text = gameFile.getTitle(), + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + minLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(8.dp) + ) + } + } +} + /** * A table arranged into columns sized to wrap the largest item. Except the * first column which takes up the remaining space left by the other columns. @@ -787,6 +974,8 @@ private fun PreviewNetplayScreen() { game = "Game name", isHosting = true, onStartGame = {}, + onGameSelected = {}, + gameFiles = emptyList(), hostInputAuthorityEnabled = true, maxBuffer = 10, onMaxBufferChanged = {}, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index d3070a51cae3..610f45b2a32d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -115,7 +115,8 @@ enum class StringSetting( "" ), NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"), - NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"); + NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"), + NETPLAY_GAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Game", ""); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt index 846fcd564e9d..8c3fb2e3f3f8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt @@ -66,7 +66,7 @@ class GameCoverKeyer : Keyer { } object CoilUtils { - private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext()) + val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext()) .components { add(GameCoverKeyer()) add(GameCoverFetcher.Factory()) diff --git a/Source/Android/gradle/libs.versions.toml b/Source/Android/gradle/libs.versions.toml index e29e0c2e1042..f4811334ac3e 100644 --- a/Source/Android/gradle/libs.versions.toml +++ b/Source/Android/gradle/libs.versions.toml @@ -35,6 +35,7 @@ androidx-cardview = { group = "androidx.cardview", name = "cardview", version.re androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } @@ -45,6 +46,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" } @@ -55,6 +57,7 @@ androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "s androidx-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "tvprovider" } androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } desugar_jdk_libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar_jdk_libs" } filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref = "filepicker" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 1dd511954ff6..527ca40f40fc 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -148,6 +148,21 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv return reinterpret_cast(server); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeChangeGame(JNIEnv* env, + jobject obj, + jobject jgame_file) +{ + auto* server = GetServerPointer(env, obj); + if (!server) + return; + + const auto& game_file = *reinterpret_cast*>( + env->GetLongField(jgame_file, IDCache::GetGameFilePointer())); + + server->ChangeGame(game_file->GetSyncIdentifier(), game_file->GetLongName()); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, jobject obj) From eb02680cb6ad8a8b700ccf37608c064fbb24fb8c Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 5 May 2026 19:19:00 +0200 Subject: [PATCH 064/270] Separate out GetExternalIPAddress helper function De-duplicates the two inline implementations and prepares it for use from Android. --- Source/Core/Core/NetPlayCommon.cpp | 15 +++++++++++++++ Source/Core/Core/NetPlayCommon.h | 2 ++ Source/Core/Core/NetPlayServer.cpp | 12 ++---------- Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp | 15 +++------------ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Source/Core/Core/NetPlayCommon.cpp b/Source/Core/Core/NetPlayCommon.cpp index f1a580cf09c3..0265d82aaaf3 100644 --- a/Source/Core/Core/NetPlayCommon.cpp +++ b/Source/Core/Core/NetPlayCommon.cpp @@ -9,6 +9,7 @@ #include #include "Common/FileUtil.h" +#include "Common/HttpRequest.h" #include "Common/IOFile.h" #include "Common/MsgHandler.h" #include "Common/SFMLHelper.h" @@ -297,4 +298,18 @@ std::optional> DecompressPacketIntoBuffer(sf::Packet& packet) return out_buffer; } + +std::string GetExternalIPAddress() +{ + Common::HttpRequest request; + // ENet does not support IPv6, so IPv4 has to be used + request.UseIPv4(); + Common::HttpRequest::Response response = + request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}}); + + if (response.has_value()) + return std::string(response->begin(), response->end()); + return ""; +} + } // namespace NetPlay diff --git a/Source/Core/Core/NetPlayCommon.h b/Source/Core/Core/NetPlayCommon.h index 0c95b49c2d75..18b8744f061b 100644 --- a/Source/Core/Core/NetPlayCommon.h +++ b/Source/Core/Core/NetPlayCommon.h @@ -20,6 +20,8 @@ using namespace std::chrono_literals; // connection is disconnected constexpr std::chrono::milliseconds PEER_TIMEOUT = 30s; +std::string GetExternalIPAddress(); + bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet); bool CompressFolderIntoPacket(const std::string& folder_path, sf::Packet& packet); bool CompressBufferIntoPacket(std::span in_buffer, sf::Packet& packet); diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index d26cc399c145..2faa5df1fa05 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -24,7 +24,6 @@ #include "Common/CommonPaths.h" #include "Common/ENet.h" #include "Common/FileUtil.h" -#include "Common/HttpRequest.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" #include "Common/SFMLHelper.h" @@ -220,16 +219,9 @@ void NetPlayServer::SetupIndex() } else { - Common::HttpRequest request; - // ENet does not support IPv6, so IPv4 has to be used - request.UseIPv4(); - Common::HttpRequest::Response response = - request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}}); - - if (!response.has_value()) + session.server_id = GetExternalIPAddress(); + if (session.server_id.empty()) return; - - session.server_id = std::string(response->begin(), response->end()); } session.EncryptID(Config::Get(Config::NETPLAY_INDEX_PASSWORD)); diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index 6cadce920325..5282fdb9b99d 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -30,7 +30,7 @@ #endif #include "Common/Config/Config.h" -#include "Common/HttpRequest.h" +#include "Core/NetPlayCommon.h" #include "Common/Logging/Log.h" #include "Common/TraversalClient.h" @@ -550,17 +550,8 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal) void NetPlayDialog::ResetExternalIP() { - m_external_ip_address = Common::Lazy([]() -> std::string { - Common::HttpRequest request; - // ENet does not support IPv6, so IPv4 has to be used - request.UseIPv4(); - Common::HttpRequest::Response response = - request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}}); - - if (response.has_value()) - return std::string(response->begin(), response->end()); - return ""; - }); + m_external_ip_address = + Common::Lazy([]() -> std::string { return NetPlay::GetExternalIPAddress(); }); } void NetPlayDialog::UpdateDiscordPresence() From 86a0f228876a101dfb3eafb08a56138eaa4b7e68 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sun, 3 May 2026 23:23:07 +0200 Subject: [PATCH 065/270] Show joining info for local and external IP addresses Doesn't support traversal yet --- .../features/netplay/NetplaySession.kt | 8 + .../features/netplay/model/JoinInfo.kt | 17 ++ .../netplay/model/NetplayViewModel.kt | 37 ++- .../features/netplay/ui/NetplayActivity.kt | 4 +- .../features/netplay/ui/NetplayScreen.kt | 231 ++++++++++++++++-- .../dolphinemu/ui/theme/DolphinTheme.kt | 36 +++ .../dolphinemu/utils/NetworkHelper.kt | 5 + .../app/src/main/res/values/strings.xml | 8 + Source/Android/jni/NetPlay/Netplay.cpp | 20 ++ 9 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 3ec1dea43f78..8af9cf98067e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -158,6 +158,10 @@ class NetplaySession( fun startGame() = nativeStartGame() + fun getPort(): Int = nativeGetPort() + + fun getExternalIpAddress(): String? = nativeGetExternalIpAddress() + fun consumeBootSessionData(): Long { return bootSessionDataPointer.also { bootSessionDataPointer = 0 @@ -239,6 +243,10 @@ class NetplaySession( private external fun nativeStartGame() + private external fun nativeGetPort(): Int + + private external fun nativeGetExternalIpAddress(): String? + // NetPlayUI callbacks @Keep diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt new file mode 100644 index 000000000000..ba350a3cafe2 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay.model + +import androidx.annotation.StringRes +import org.dolphinemu.dolphinemu.R + +enum class JoinInfoType(@StringRes val labelId: Int) { + EXTERNAL(R.string.netplay_address_type_external), + LOCAL(R.string.netplay_address_type_local), +} + +sealed class JoinAddress { + data object Loading : JoinAddress() + data class Loaded(val address: String) : JoinAddress() + data class Unknown(val retry: () -> Unit) : JoinAddress() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 50edaba597cf..923cfc314d75 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -20,15 +21,25 @@ import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.utils.NetworkHelper class NetplayViewModel( private val netplaySession: NetplaySession, + private val networkHelper: NetworkHelper, ) : ViewModel() { val launchGame = netplaySession.launchGame val isHosting = netplaySession.isHosting + private val _joinAddresses = MutableStateFlow( + mapOf( + JoinInfoType.EXTERNAL to JoinAddress.Loading, + JoinInfoType.LOCAL to getLocalIp(), + ) + ) + val joinAddresses = _joinAddresses.asStateFlow() + val connectionLost = netplaySession.connectionLost val players = netplaySession.players @@ -61,6 +72,7 @@ class NetplayViewModel( init { if (netplaySession.isHosting) { setInitialGame() + fetchExternalIp() } } @@ -88,6 +100,24 @@ class NetplayViewModel( netplaySession.changeGame(gameFile) } + private fun getLocalIp(): JoinAddress { + val localIp = networkHelper.getLocalIpString() + ?: return JoinAddress.Unknown { _joinAddresses.value += JoinInfoType.LOCAL to getLocalIp() } + val port = netplaySession.getPort() + return JoinAddress.Loaded("$localIp:$port") + } + + private fun fetchExternalIp() { + _joinAddresses.value += JoinInfoType.EXTERNAL to JoinAddress.Loading + viewModelScope.launch(Dispatchers.IO) { + val ip = netplaySession.getExternalIpAddress() + val port = netplaySession.getPort() + val address = if (ip != null) JoinAddress.Loaded("$ip:$port") + else JoinAddress.Unknown { fetchExternalIp() } + _joinAddresses.value += JoinInfoType.EXTERNAL to address + } + } + private fun setInitialGame() { val game = gameFiles.value .find { it.getGameId() == StringSetting.NETPLAY_GAME.string } @@ -108,10 +138,13 @@ class NetplayViewModel( } } - class Factory(private val session: NetplaySession) : ViewModelProvider.Factory { + class Factory( + private val session: NetplaySession, + private val networkHelper: NetworkHelper, + ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return NetplayViewModel(session) as T + return NetplayViewModel(session, networkHelper) as T } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index 55888dde6de2..a9bf0309a59c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -21,6 +21,7 @@ import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme +import org.dolphinemu.dolphinemu.utils.NetworkHelper import org.dolphinemu.dolphinemu.utils.ThemeHelper class NetplayActivity : AppCompatActivity(), ThemeProvider { @@ -37,7 +38,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { return } - val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session))[NetplayViewModel::class.java] + val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session, NetworkHelper))[NetplayViewModel::class.java] viewModel.launchGame .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) @@ -62,6 +63,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { onMaxBufferChanged = viewModel::setMaxBuffer, saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value, gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value, + joinAddresses = viewModel.joinAddresses.collectAsState().value, ) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 86cbfe512932..eeba0f154120 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -2,10 +2,11 @@ package org.dolphinemu.dolphinemu.features.netplay.ui +import android.content.Intent import android.content.res.Configuration -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -32,11 +34,17 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -45,6 +53,7 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -62,7 +71,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration @@ -84,6 +92,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress +import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress +import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -92,6 +102,7 @@ import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme +import org.dolphinemu.dolphinemu.ui.theme.ReadOnlyTextField import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.Locale @@ -113,6 +124,7 @@ fun NetplayScreen( players: List, saveTransferProgress: SaveTransferProgress?, gameDigestProgress: GameDigestProgress?, + joinAddresses: Map, ) { Scaffold( topBar = { @@ -141,8 +153,10 @@ fun NetplayScreen( .consumeWindowInsets(innerPadding) .padding(innerPadding) + // State which must live above the landscape/portrait split. var showChat by rememberSaveable { mutableStateOf(false) } var showGamePicker by rememberSaveable { mutableStateOf(false) } + var selectedJoinInfoType by rememberSaveable { mutableStateOf(JoinInfoType.EXTERNAL) } if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( @@ -160,6 +174,9 @@ fun NetplayScreen( hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, modifier = modifier ) } else { @@ -178,6 +195,9 @@ fun NetplayScreen( hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, modifier = modifier ) } @@ -243,6 +263,9 @@ private fun PortraitContent( hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, + joinAddresses: Map, + selectedJoinInfoType: JoinInfoType, + onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -273,6 +296,9 @@ private fun PortraitContent( maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, isHosting = isHosting, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = onSelectedJoinInfoTypeChanged, modifier = Modifier .padding(horizontal = DolphinTheme.scaffoldPadding), ) @@ -299,10 +325,14 @@ private fun LandscapeContent( hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, + joinAddresses: Map, + selectedJoinInfoType: JoinInfoType, + onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier + .padding(horizontal = DolphinTheme.scaffoldPadding) ) { Chat( messages = messages, @@ -312,9 +342,10 @@ private fun LandscapeContent( modifier = Modifier .weight(1f) .fillMaxHeight() - .padding(horizontal = DolphinTheme.scaffoldPadding) ) + Spacer(modifier = Modifier.width(16.dp)) + Column( modifier = Modifier .weight(1f) @@ -331,8 +362,10 @@ private fun LandscapeContent( maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, isHosting = isHosting, + joinAddresses = joinAddresses, + selectedJoinInfoType = selectedJoinInfoType, + onSelectedJoinInfoTypeChanged = onSelectedJoinInfoTypeChanged, modifier = Modifier - .padding(horizontal = DolphinTheme.scaffoldPadding) ) if (isHosting) { @@ -354,6 +387,9 @@ private fun PlayersAndSettings( maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, isHosting: Boolean, + joinAddresses: Map, + selectedJoinInfoType: JoinInfoType, + onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -368,6 +404,16 @@ private fun PlayersAndSettings( isHosting = isHosting, ) + if (isHosting) { + MenuSpacer() + + JoinAddressSection( + joinAddresses = joinAddresses, + selectedType = selectedJoinInfoType, + onSelectedTypeChanged = onSelectedJoinInfoTypeChanged, + ) + } + MenuSpacer() OutlinedBox( @@ -479,6 +525,7 @@ private fun Chat( OutlinedBox( onClick = { onShowBottomSheetChanged(true) }, label = { Text(stringResource(R.string.netplay_chat_label)) }, + fadeContentTop = true, modifier = modifier ) { LazyColumn( @@ -532,26 +579,16 @@ private fun GamePicker( } } - Box( + ReadOnlyTextField( + value = game, + label = stringResource(R.string.netplay_game_label), + onClick = if (isHosting) { + { onShowGamePickerChanged(true) } + } else { + null + }, modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = game, - onValueChange = {}, - label = { Text(stringResource(R.string.netplay_game_label)) }, - readOnly = true, - modifier = Modifier.fillMaxWidth() - ) - if (isHosting) { - Box( - modifier = Modifier - .matchParentSize() - .padding(top = 8.dp) - .clip(MaterialTheme.shapes.extraSmall) - .clickable { onShowGamePickerChanged(true) } - ) - } - } + ) } @Composable @@ -609,6 +646,150 @@ private fun GameGridItem( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun JoinAddressSection( + joinAddresses: Map, + selectedType: JoinInfoType, + onSelectedTypeChanged: (JoinInfoType) -> Unit, +) { + val address = joinAddresses[selectedType] ?: joinAddresses.values.first() + + @Suppress("UnusedBoxWithConstraintsScope") + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + if (maxWidth > 392.dp) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + JoinInfoDropdown( + joinAddresses = joinAddresses, + selectedType = selectedType, + onSelectedTypeChanged = onSelectedTypeChanged, + modifier = Modifier.weight(0.39f), + ) + AddressRow( + address = address, + modifier = Modifier.weight(0.61f), + ) + } + } else { + Column(modifier = Modifier.fillMaxWidth()) { + JoinInfoDropdown( + joinAddresses = joinAddresses, + selectedType = selectedType, + onSelectedTypeChanged = onSelectedTypeChanged, + modifier = Modifier.fillMaxWidth(), + ) + MenuSpacer() + AddressRow( + address = address, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun JoinInfoDropdown( + joinAddresses: Map, + selectedType: JoinInfoType, + onSelectedTypeChanged: (JoinInfoType) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = modifier, + ) { + OutlinedTextField( + value = stringResource(selectedType.labelId), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.netplay_host_address_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + joinAddresses.keys.forEach { type -> + DropdownMenuItem( + text = { Text(stringResource(type.labelId)) }, + onClick = { + onSelectedTypeChanged(type) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + } +} + +@Composable +private fun AddressRow( + address: JoinAddress, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + ReadOnlyTextField( + value = when (address) { + is JoinAddress.Loading -> stringResource(R.string.netplay_address_loading) + is JoinAddress.Loaded -> address.address + is JoinAddress.Unknown -> stringResource(R.string.netplay_address_unknown) + }, + label = stringResource(R.string.netplay_address_label), + onClick = when (address) { + is JoinAddress.Loaded -> { + { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, address.address) + } + context.startActivity(Intent.createChooser(intent, null)) + } + } + + is JoinAddress.Unknown -> address.retry + is JoinAddress.Loading -> null + }, + textStyle = if (address is JoinAddress.Loading) { + LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) + } else { + null + }, + trailingIcon = { + when (address) { + is JoinAddress.Loaded -> Icon( + imageVector = Icons.Filled.Share, + contentDescription = stringResource(R.string.netplay_address_share), + ) + + is JoinAddress.Unknown -> Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.netplay_address_retry), + ) + + is JoinAddress.Loading -> CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + ) + } + }, + modifier = modifier, + ) +} + /** * A table arranged into columns sized to wrap the largest item. Except the * first column which takes up the remaining space left by the other columns. @@ -981,6 +1162,10 @@ private fun PreviewNetplayScreen() { onMaxBufferChanged = {}, saveTransferProgress = null, gameDigestProgress = null, + joinAddresses = mapOf( + JoinInfoType.EXTERNAL to JoinAddress.Loaded("203.0.113.1:2626"), + JoinInfoType.LOCAL to JoinAddress.Loaded("192.168.1.5:2626"), + ), // saveTransferProgress = SaveTransferProgress( // title = "Title", // totalSize = 1024L, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index d25a34586275..c5fa7463469a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -17,8 +17,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @@ -28,6 +31,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.google.android.material.color.MaterialColors @@ -205,3 +209,35 @@ fun OutlinedBox( } } } + +@Composable +fun ReadOnlyTextField( + value: String, + label: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, + textStyle: TextStyle? = null, +) { + Box(modifier = modifier) { + OutlinedTextField( + value = value, + onValueChange = {}, + readOnly = true, + singleLine = true, + label = { Text(label) }, + trailingIcon = trailingIcon, + textStyle = textStyle ?: LocalTextStyle.current, + modifier = Modifier.fillMaxWidth() + ) + if (onClick != null) { + Box( + modifier = Modifier + .matchParentSize() + .padding(top = 8.dp) + .clip(MaterialTheme.shapes.extraSmall) + .clickable(onClick = onClick) + ) + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt index 7bd2068ee2ee..231f15fda1ea 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt @@ -75,4 +75,9 @@ object NetworkHelper { 0 } } + + @JvmStatic + fun getLocalIpString(): String? { + return getIPv4Link()?.address?.hostAddress + } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 9c89d3aed749..20c78eddc5e3 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1016,4 +1016,12 @@ It can efficiently compress both junk data and encrypted Wii data. Close Port Forward port (UPnP) + Join info + Address + External IP + Local IP + Loading… + Unknown + Share address + Retry diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 527ca40f40fc..9192404042f2 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -8,6 +8,7 @@ #include #include "Common/CommonTypes.h" +#include "Core/NetPlayCommon.h" #include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" @@ -174,6 +175,25 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(J server->RequestStartGame(); } +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPort(JNIEnv* env, + jobject obj) +{ + if (auto* server = GetServerPointer(env, obj)) + return static_cast(server->GetPort()); + return 0; +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetExternalIpAddress( + JNIEnv* env, jobject) +{ + std::string ip = NetPlay::GetExternalIPAddress(); + if (ip.empty()) + return nullptr; + return ToJString(env, ip); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, jobject, From 86de5ef4b0bd210aab15051a0e9a4098c5ecaa2a Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 5 May 2026 18:50:04 +0200 Subject: [PATCH 066/270] Handle traversal state changes and errors Traversal connections show in the joining info UI. Non fatal errors show the retry button. Fatal errors end the netplay session. --- .../features/netplay/NetplaySession.kt | 38 +++++++- .../features/netplay/model/JoinInfo.kt | 1 + .../netplay/model/NetplayViewModel.kt | 51 +++++++++-- .../features/netplay/model/TraversalState.kt | 18 ++++ .../features/netplay/ui/NetplayActivity.kt | 1 + .../features/netplay/ui/NetplayScreen.kt | 34 ++++++- .../app/src/main/res/values/strings.xml | 8 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 9 ++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 89 ++++++++++++++++++- Source/Android/jni/NetPlay/Netplay.cpp | 9 ++ 11 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 8af9cf98067e..d8f2ce8860bc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -25,11 +25,12 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress -import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress +import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState import org.dolphinemu.dolphinemu.features.settings.model.StringSetting +import org.dolphinemu.dolphinemu.model.GameFile class NetplaySession( private val onClosed: (NetplaySession) -> Unit, @@ -117,6 +118,15 @@ class NetplaySession( private val _gameDigestProgress = MutableStateFlow(null) val gameDigestProgress = _gameDigestProgress.asStateFlow() + private val _traversalState = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val traversalState = _traversalState.asSharedFlow() + + private val _fatalTraversalError = Channel(Channel.CONFLATED) + val fatalTraversalError = _fatalTraversalError.receiveAsFlow() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { if (isClosed) throw IllegalStateException("Cannot join a closed session") @@ -148,7 +158,7 @@ class NetplaySession( } fun sendMessage(message: String) { - _chatMessages.tryEmit( "$nickName: $message") + _chatMessages.tryEmit("$nickName: $message") nativeSendMessage(message) } @@ -162,6 +172,8 @@ class NetplaySession( fun getExternalIpAddress(): String? = nativeGetExternalIpAddress() + fun reconnectTraversal() = nativeReconnectTraversal() + fun consumeBootSessionData(): Long { return bootSessionDataPointer.also { bootSessionDataPointer = 0 @@ -247,6 +259,8 @@ class NetplaySession( private external fun nativeGetExternalIpAddress(): String? + private external fun nativeReconnectTraversal() + // NetPlayUI callbacks @Keep @@ -390,6 +404,26 @@ class NetplaySession( fun onAbortGameDigest() { _gameDigestProgress.value = null } + + @Keep + fun onTraversalStateChanged( + state: Int, + hostCode: String?, + externalAddress: String?, + failureReason: String?, + ) { + val traversalState = when (state) { + 0 -> TraversalState.Connecting + 1 -> TraversalState.Connected(hostCode!!, externalAddress!!) + 2 -> TraversalState.Failure(failureReason!!) + else -> return + } + _traversalState.tryEmit(traversalState) + + if (failureReason == "BadHost" || failureReason == "VersionTooOld") { + _fatalTraversalError.trySend(TraversalState.Failure(failureReason)) + } + } } private fun Channel.flush() { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt index ba350a3cafe2..7403f06f32b3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt @@ -6,6 +6,7 @@ import androidx.annotation.StringRes import org.dolphinemu.dolphinemu.R enum class JoinInfoType(@StringRes val labelId: Int) { + ROOM_ID(R.string.netplay_address_type_room_id), EXTERNAL(R.string.netplay_address_type_external), LOCAL(R.string.netplay_address_type_local), } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 923cfc314d75..4ebea7e283c9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.NetplaySession @@ -28,20 +30,29 @@ class NetplayViewModel( private val networkHelper: NetworkHelper, ) : ViewModel() { + private val isTraversal = StringSetting.NETPLAY_TRAVERSAL_CHOICE.string == "traversal" + val launchGame = netplaySession.launchGame val isHosting = netplaySession.isHosting private val _joinAddresses = MutableStateFlow( - mapOf( - JoinInfoType.EXTERNAL to JoinAddress.Loading, - JoinInfoType.LOCAL to getLocalIp(), - ) + buildMap { + if (isHosting) { + if (isTraversal) { + put(JoinInfoType.ROOM_ID, JoinAddress.Loading) + } + put(JoinInfoType.EXTERNAL, JoinAddress.Loading) + put(JoinInfoType.LOCAL, getLocalIp()) + } + } ) val joinAddresses = _joinAddresses.asStateFlow() val connectionLost = netplaySession.connectionLost + val fatalTraversalError = netplaySession.fatalTraversalError + val players = netplaySession.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) @@ -72,7 +83,11 @@ class NetplayViewModel( init { if (netplaySession.isHosting) { setInitialGame() - fetchExternalIp() + if (isTraversal) { + collectTraversalState() + } else { + fetchExternalIp() + } } } @@ -118,6 +133,32 @@ class NetplayViewModel( } } + private fun collectTraversalState() { + val retry = { netplaySession.reconnectTraversal() } + netplaySession.traversalState.onEach { state -> + when (state) { + is TraversalState.Connecting -> { + _joinAddresses.value += mapOf( + JoinInfoType.ROOM_ID to JoinAddress.Loading, + JoinInfoType.EXTERNAL to JoinAddress.Loading, + ) + } + is TraversalState.Connected -> { + _joinAddresses.value += mapOf( + JoinInfoType.ROOM_ID to JoinAddress.Loaded(state.hostCode), + JoinInfoType.EXTERNAL to JoinAddress.Loaded(state.externalAddress), + ) + } + is TraversalState.Failure -> { + _joinAddresses.value += mapOf( + JoinInfoType.ROOM_ID to JoinAddress.Unknown(retry), + JoinInfoType.EXTERNAL to JoinAddress.Unknown(retry), + ) + } + } + }.launchIn(viewModelScope) + } + private fun setInitialGame() { val game = gameFiles.value .find { it.getGameId() == StringSetting.NETPLAY_GAME.string } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt new file mode 100644 index 000000000000..9b574e4f6327 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt @@ -0,0 +1,18 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +import android.content.Context +import org.dolphinemu.dolphinemu.R + +sealed class TraversalState { + data object Connecting : TraversalState() + + data class Connected(val hostCode: String, val externalAddress: String) : TraversalState() + + data class Failure(val reason: String) : TraversalState() { + fun message(context: Context) = when (reason) { + "BadHost" -> context.getString(R.string.netplay_traversal_error_bad_host) + "VersionTooOld" -> context.getString(R.string.netplay_traversal_error_version_too_old) + else -> reason + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index a9bf0309a59c..e45ba7e7c1d9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -50,6 +50,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { NetplayScreen( onBackClicked = { finish() }, connectionLost = viewModel.connectionLost, + fatalTraversalError = viewModel.fatalTraversalError, messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index eeba0f154120..71b427a8c884 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -97,6 +97,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress +import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer @@ -112,6 +113,7 @@ fun NetplayScreen( onBackClicked: () -> Unit, isHosting: Boolean, connectionLost: Flow, + fatalTraversalError: Flow, messages: List, onSendMessage: (String) -> Unit, game: String, @@ -156,7 +158,9 @@ fun NetplayScreen( // State which must live above the landscape/portrait split. var showChat by rememberSaveable { mutableStateOf(false) } var showGamePicker by rememberSaveable { mutableStateOf(false) } - var selectedJoinInfoType by rememberSaveable { mutableStateOf(JoinInfoType.EXTERNAL) } + var selectedJoinInfoType by rememberSaveable { + mutableStateOf(joinAddresses.keys.firstOrNull() ?: JoinInfoType.EXTERNAL) + } if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( @@ -207,6 +211,11 @@ fun NetplayScreen( connectionLost.collect { showConnectionLostDialog = true } } + var traversalError by rememberSaveable { mutableStateOf(null) } + LaunchedEffect(Unit) { + fatalTraversalError.collect { traversalError = it } + } + var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) } if (saveTransferProgress == null) { dismissSaveTransferProgressDialog = false @@ -217,6 +226,8 @@ fun NetplayScreen( dismissGameDigestDialog = false } + val currentTraversalError = traversalError + when { showConnectionLostDialog -> { AlertDialog( @@ -230,6 +241,18 @@ fun NetplayScreen( ) } + currentTraversalError != null -> { + AlertDialog( + text = { Text(currentTraversalError.message(LocalContext.current)) }, + confirmButton = { + TextButton(onClick = onBackClicked) { + Text(stringResource(R.string.ok)) + } + }, + onDismissRequest = onBackClicked, + ) + } + saveTransferProgress != null && !dismissSaveTransferProgressDialog -> { SaveTransferProgressDialog( saveTransferProgress = saveTransferProgress, @@ -669,6 +692,7 @@ private fun JoinAddressSection( modifier = Modifier.weight(0.39f), ) AddressRow( + joinInfoType = selectedType, address = address, modifier = Modifier.weight(0.61f), ) @@ -683,6 +707,7 @@ private fun JoinAddressSection( ) MenuSpacer() AddressRow( + joinInfoType = selectedType, address = address, modifier = Modifier.fillMaxWidth(), ) @@ -737,6 +762,7 @@ private fun JoinInfoDropdown( @Composable private fun AddressRow( + joinInfoType: JoinInfoType, address: JoinAddress, modifier: Modifier = Modifier, ) { @@ -748,7 +774,10 @@ private fun AddressRow( is JoinAddress.Loaded -> address.address is JoinAddress.Unknown -> stringResource(R.string.netplay_address_unknown) }, - label = stringResource(R.string.netplay_address_label), + label = stringResource( + if (joinInfoType == JoinInfoType.ROOM_ID) R.string.netplay_code_label + else R.string.netplay_address_label + ), onClick = when (address) { is JoinAddress.Loaded -> { { @@ -1128,6 +1157,7 @@ private fun PreviewNetplayScreen() { NetplayScreen( onBackClicked = {}, connectionLost = emptyFlow(), + fatalTraversalError = emptyFlow(), players = listOf( Player( pid = 1, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 20c78eddc5e3..fbc664f4ef28 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1018,8 +1018,12 @@ It can efficiently compress both junk data and encrypted Wii data. Forward port (UPnP) Join info Address - External IP - Local IP + Code + Room + External + Local + Couldn\'t look up central server + Dolphin is too old for traversal server Loading… Unknown Share address diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 41d70554714c..db05bda5d5c1 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -49,6 +49,7 @@ static jmethodID s_netplay_on_show_game_digest_dialog; static jmethodID s_netplay_on_set_game_digest_progress; static jmethodID s_netplay_on_set_game_digest_result; static jmethodID s_netplay_on_abort_game_digest; +static jmethodID s_netplay_on_traversal_state_changed; static jclass s_netplay_player_class; static jmethodID s_netplay_player_constructor; @@ -363,6 +364,11 @@ jmethodID GetNetplayOnAbortGameDigest() return s_netplay_on_abort_game_digest; } +jmethodID GetNetplayOnTraversalStateChanged() +{ + return s_netplay_on_traversal_state_changed; +} + jclass GetNetplayPlayerClass() { return s_netplay_player_class; @@ -813,6 +819,9 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(netplay_class, "onSetGameDigestResult", "(ILjava/lang/String;)V"); s_netplay_on_abort_game_digest = env->GetMethodID(netplay_class, "onAbortGameDigest", "()V"); + s_netplay_on_traversal_state_changed = env->GetMethodID( + netplay_class, "onTraversalStateChanged", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 51a5ae02bc2e..064b37df4451 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -52,6 +52,7 @@ jmethodID GetNetplayOnShowGameDigestDialog(); jmethodID GetNetplayOnSetGameDigestProgress(); jmethodID GetNetplayOnSetGameDigestResult(); jmethodID GetNetplayOnAbortGameDigest(); +jmethodID GetNetplayOnTraversalStateChanged(); jclass GetNetplayPlayerClass(); jmethodID GetNetplayPlayerConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index ecbf168fb5fe..ce9336ff15b1 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -1,15 +1,62 @@ // Copyright 2003 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "UICommon/GameFile.h" #include "UICommon/UICommon.h" #include "NetPlayUICallbacks.h" +#include "Common/TraversalClient.h" #include "Core/Boot/Boot.h" #include "Core/Core.h" #include "Core/System.h" #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" +namespace +{ +std::string InetAddressToString(const Common::TraversalInetAddress& addr) +{ + std::string ip; + + if (addr.isIPV6) + { + ip = "IPv6-Not-Implemented"; + } + else + { + const auto ipv4 = reinterpret_cast(addr.address); + ip = std::to_string(ipv4[0]); + for (u32 i = 1; i != 4; ++i) + { + ip += "."; + ip += std::to_string(ipv4[i]); + } + } + + return ip + ":" + std::to_string(ntohs(addr.port)); +} + +const char* FailureReasonToString(Common::TraversalClient::FailureReason reason) +{ + switch (reason) + { + case Common::TraversalClient::FailureReason::BadHost: + return "BadHost"; + case Common::TraversalClient::FailureReason::VersionTooOld: + return "VersionTooOld"; + case Common::TraversalClient::FailureReason::ServerForgotAboutUs: + return "ServerForgotAboutUs"; + case Common::TraversalClient::FailureReason::SocketSendError: + return "SocketSendError"; + case Common::TraversalClient::FailureReason::ResendTimeout: + return "ResendTimeout"; + default: + return "Unknown"; + } +} +} // namespace + namespace NetPlay { NetPlayUICallbacks::NetPlayUICallbacks(jobject netplay_session, @@ -242,8 +289,48 @@ void NetPlayUICallbacks::OnConnectionError(const std::string& message) env->DeleteLocalRef(netplay_session); } +// No-op — all error info is captured by OnTraversalStateChanged which always fires alongside. void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {} -void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State) {} + +void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State state) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + jstring host_code = nullptr; + jstring external_address = nullptr; + jstring failure_reason = nullptr; + + if (Common::g_TraversalClient) + { + if (state == Common::TraversalClient::State::Connected) + { + const auto host_id = Common::g_TraversalClient->GetHostID(); + host_code = ToJString(env, std::string(host_id.begin(), host_id.end())); + external_address = + ToJString(env, InetAddressToString(Common::g_TraversalClient->GetExternalAddress())); + } + else if (state == Common::TraversalClient::State::Failure) + { + failure_reason = + ToJString(env, FailureReasonToString(Common::g_TraversalClient->GetFailureReason())); + } + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnTraversalStateChanged(), + static_cast(state), host_code, external_address, failure_reason); + } + + if (host_code) + env->DeleteLocalRef(host_code); + if (external_address) + env->DeleteLocalRef(external_address); + if (failure_reason) + env->DeleteLocalRef(failure_reason); + env->DeleteLocalRef(netplay_session); +} + void NetPlayUICallbacks::OnGameStartAborted() {} void NetPlayUICallbacks::OnGolferChanged(bool, const std::string&) {} void NetPlayUICallbacks::OnTtlDetermined(u8) {} diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 9192404042f2..8ed026c9c80a 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -8,6 +8,7 @@ #include #include "Common/CommonTypes.h" +#include "Common/TraversalClient.h" #include "Core/NetPlayCommon.h" #include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" @@ -194,6 +195,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetExternal return ToJString(env, ip); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReconnectTraversal(JNIEnv*, + jobject) +{ + if (Common::g_TraversalClient) + Common::g_TraversalClient->ReconnectToServer(); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, jobject, From 1c2806dfe9d6d644a1423ccced319d0ac7dea632 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 5 May 2026 21:50:04 +0200 Subject: [PATCH 067/270] Add host buffer settings for fair input delay mode Also rename max buffer to client buffer for better consistency with settings and c++ --- .../features/netplay/NetplaySession.kt | 10 ++- .../netplay/model/NetplayViewModel.kt | 21 ++++-- .../features/netplay/ui/NetplayActivity.kt | 6 +- .../features/netplay/ui/NetplayScreen.kt | 72 +++++++++++++------ .../features/settings/model/IntSetting.kt | 6 ++ .../app/src/main/res/values/strings.xml | 3 +- Source/Android/jni/NetPlay/Netplay.cpp | 10 ++- 7 files changed, 93 insertions(+), 35 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index d8f2ce8860bc..c5795edf6346 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -49,7 +49,7 @@ class NetplaySession( @Volatile var isClosed = false private set - + val isHosting: Boolean get() = netPlayServerPointer != 0L @@ -162,7 +162,9 @@ class NetplaySession( nativeSendMessage(message) } - fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + fun adjustClientPadBufferSize(buffer: Int) = nativeAdjustClientPadBufferSize(buffer) + + fun adjustServerPadBufferSize(buffer: Int) = nativeAdjustServerPadBufferSize(buffer) fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile) @@ -241,7 +243,9 @@ class NetplaySession( private external fun nativeSendMessage(message: String) - private external fun nativeAdjustPadBufferSize(buffer: Int) + private external fun nativeAdjustClientPadBufferSize(buffer: Int) + + private external fun nativeAdjustServerPadBufferSize(buffer: Int) private external fun nativeReleaseUICallbacks(pointer: Long) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 4ebea7e283c9..ea8ac9dea511 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -65,8 +65,11 @@ class NetplayViewModel( val hostInputAuthority = netplaySession.hostInputAuthorityEnabled .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) - private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) - val maxBuffer = _maxBuffer.asStateFlow() + private val _buffer = MutableStateFlow(IntSetting.NETPLAY_BUFFER_SIZE.int) + val buffer = _buffer.asStateFlow() + + private val _clientBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) + val clientBuffer = _clientBuffer.asStateFlow() val gameFiles = GameFileCacheManager.getGameFiles().asFlow() .map { it.toList() } @@ -104,10 +107,16 @@ class NetplayViewModel( netplaySession.sendMessage(trimmedMessage) } - fun setMaxBuffer(buffer: Int) { - _maxBuffer.value = buffer - IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, buffer) - netplaySession.adjustPadBufferSize(buffer) + fun setBuffer(value: Int) { + _buffer.value = value + IntSetting.NETPLAY_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, value) + netplaySession.adjustServerPadBufferSize(value) + } + + fun setClientBuffer(value: Int) { + _clientBuffer.value = value + IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, value) + netplaySession.adjustClientPadBufferSize(value) } fun changeGame(gameFile: GameFile) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index e45ba7e7c1d9..bae20aee4682 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -60,8 +60,10 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { gameFiles = viewModel.gameFiles.collectAsState().value, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, - maxBuffer = viewModel.maxBuffer.collectAsState().value, - onMaxBufferChanged = viewModel::setMaxBuffer, + buffer = viewModel.buffer.collectAsState().value, + onBufferChanged = viewModel::setBuffer, + clientBuffer = viewModel.clientBuffer.collectAsState().value, + onClientBufferChanged = viewModel::setClientBuffer, saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value, gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value, joinAddresses = viewModel.joinAddresses.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 71b427a8c884..e68e6ff7d201 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -121,8 +121,10 @@ fun NetplayScreen( onGameSelected: (GameFile) -> Unit, gameFiles: List, hostInputAuthorityEnabled: Boolean, - maxBuffer: Int, - onMaxBufferChanged: (Int) -> Unit, + buffer: Int, + onBufferChanged: (Int) -> Unit, + clientBuffer: Int, + onClientBufferChanged: (Int) -> Unit, players: List, saveTransferProgress: SaveTransferProgress?, gameDigestProgress: GameDigestProgress?, @@ -176,8 +178,10 @@ fun NetplayScreen( onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, - maxBuffer = maxBuffer, - onMaxBufferChanged = onMaxBufferChanged, + buffer = buffer, + onBufferChanged = onBufferChanged, + clientBuffer = clientBuffer, + onClientBufferChanged = onClientBufferChanged, joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, @@ -197,8 +201,10 @@ fun NetplayScreen( onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, - maxBuffer = maxBuffer, - onMaxBufferChanged = onMaxBufferChanged, + buffer = buffer, + onBufferChanged = onBufferChanged, + clientBuffer = clientBuffer, + onClientBufferChanged = onClientBufferChanged, joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, @@ -284,8 +290,10 @@ private fun PortraitContent( onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, - maxBuffer: Int, - onMaxBufferChanged: (Int) -> Unit, + buffer: Int, + onBufferChanged: (Int) -> Unit, + clientBuffer: Int, + onClientBufferChanged: (Int) -> Unit, joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, @@ -316,8 +324,10 @@ private fun PortraitContent( onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, - maxBuffer = maxBuffer, - onMaxBufferChanged = onMaxBufferChanged, + buffer = buffer, + onBufferChanged = onBufferChanged, + clientBuffer = clientBuffer, + onClientBufferChanged = onClientBufferChanged, isHosting = isHosting, joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, @@ -346,8 +356,10 @@ private fun LandscapeContent( onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, - maxBuffer: Int, - onMaxBufferChanged: (Int) -> Unit, + buffer: Int, + onBufferChanged: (Int) -> Unit, + clientBuffer: Int, + onClientBufferChanged: (Int) -> Unit, joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, @@ -382,8 +394,10 @@ private fun LandscapeContent( onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, - maxBuffer = maxBuffer, - onMaxBufferChanged = onMaxBufferChanged, + buffer = buffer, + onBufferChanged = onBufferChanged, + clientBuffer = clientBuffer, + onClientBufferChanged = onClientBufferChanged, isHosting = isHosting, joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, @@ -407,8 +421,10 @@ private fun PlayersAndSettings( onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, - maxBuffer: Int, - onMaxBufferChanged: (Int) -> Unit, + buffer: Int, + onBufferChanged: (Int) -> Unit, + clientBuffer: Int, + onClientBufferChanged: (Int) -> Unit, isHosting: Boolean, joinAddresses: Map, selectedJoinInfoType: JoinInfoType, @@ -459,13 +475,23 @@ private fun PlayersAndSettings( ) } - if (hostInputAuthorityEnabled) { + if (isHosting && !hostInputAuthorityEnabled) { MenuSpacer() BufferInput( - value = maxBuffer, - onValueChange = onMaxBufferChanged, - label = stringResource(R.string.netplay_max_buffer), + value = buffer, + onValueChange = onBufferChanged, + label = stringResource(R.string.netplay_buffer), + ) + } + + if (!isHosting && hostInputAuthorityEnabled) { + MenuSpacer() + + BufferInput( + value = clientBuffer, + onValueChange = onClientBufferChanged, + label = stringResource(R.string.netplay_client_buffer), ) } } @@ -1188,8 +1214,10 @@ private fun PreviewNetplayScreen() { onGameSelected = {}, gameFiles = emptyList(), hostInputAuthorityEnabled = true, - maxBuffer = 10, - onMaxBufferChanged = {}, + buffer = 5, + onBufferChanged = {}, + clientBuffer = 10, + onClientBufferChanged = {}, saveTransferProgress = null, gameDigestProgress = null, joinAddresses = mapOf( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt index 85f265442b16..628e286b49a0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt @@ -146,6 +146,12 @@ enum class IntSetting( Settings.SECTION_INI_NETPLAY, "BufferSizeClient", 1 + ), + NETPLAY_BUFFER_SIZE( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_NETPLAY, + "BufferSize", + 5 ); override val isOverridden: Boolean diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index fbc664f4ef28..09abe850fb7c 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1008,7 +1008,8 @@ It can efficiently compress both junk data and encrypted Wii data. Name Ping Mapping - Max buffer + Buffer + Max buffer Netplay connection lost Close The hashes match diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 8ed026c9c80a..e845f8ec6072 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -50,7 +50,7 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustPadBufferSize(JNIEnv* env, +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustClientPadBufferSize(JNIEnv* env, jobject obj, jint buffer) { @@ -58,6 +58,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustPadBu client->AdjustPadBufferSize(static_cast(buffer)); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustServerPadBufferSize( + JNIEnv* env, jobject obj, jint buffer) +{ + if (auto* server = GetServerPointer(env, obj)) + server->AdjustPadBufferSize(static_cast(buffer)); +} + JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeCreateUICallbacks(JNIEnv* env, jobject obj) From 76ae44569415c0ffb674d184312ea898aab92a18 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 6 May 2026 09:46:33 +0200 Subject: [PATCH 068/270] Add network mode picker for host --- .../features/netplay/NetplaySession.kt | 4 ++ .../netplay/model/NetplayViewModel.kt | 11 +++ .../features/netplay/model/NetworkMode.kt | 18 +++++ .../features/netplay/ui/NetplayActivity.kt | 2 + .../features/netplay/ui/NetplayScreen.kt | 69 +++++++++++++++++++ .../features/settings/model/StringSetting.kt | 3 +- .../app/src/main/res/values/strings.xml | 4 ++ Source/Android/jni/NetPlay/Netplay.cpp | 8 +++ 8 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index c5795edf6346..0ba5d409b0ed 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -162,6 +162,8 @@ class NetplaySession( nativeSendMessage(message) } + fun setHostInputAuthority(enable: Boolean) = nativeSetHostInputAuthority(enable) + fun adjustClientPadBufferSize(buffer: Int) = nativeAdjustClientPadBufferSize(buffer) fun adjustServerPadBufferSize(buffer: Int) = nativeAdjustServerPadBufferSize(buffer) @@ -243,6 +245,8 @@ class NetplaySession( private external fun nativeSendMessage(message: String) + private external fun nativeSetHostInputAuthority(enable: Boolean) + private external fun nativeAdjustClientPadBufferSize(buffer: Int) private external fun nativeAdjustServerPadBufferSize(buffer: Int) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index ea8ac9dea511..9f9b422f4b69 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -65,6 +65,11 @@ class NetplayViewModel( val hostInputAuthority = netplaySession.hostInputAuthorityEnabled .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + private val _networkMode = MutableStateFlow( + NetworkMode.fromConfigValue(StringSetting.NETPLAY_NETWORK_MODE.string) + ) + val networkMode = _networkMode.asStateFlow() + private val _buffer = MutableStateFlow(IntSetting.NETPLAY_BUFFER_SIZE.int) val buffer = _buffer.asStateFlow() @@ -107,6 +112,12 @@ class NetplayViewModel( netplaySession.sendMessage(trimmedMessage) } + fun setNetworkMode(mode: NetworkMode) { + _networkMode.value = mode + StringSetting.NETPLAY_NETWORK_MODE.setString(NativeConfig.LAYER_BASE, mode.configValue) + netplaySession.setHostInputAuthority(mode.isHostInputAuthority) + } + fun setBuffer(value: Int) { _buffer.value = value IntSetting.NETPLAY_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, value) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt new file mode 100644 index 000000000000..55d29f5f170e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt @@ -0,0 +1,18 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +import androidx.annotation.StringRes +import org.dolphinemu.dolphinemu.R + +enum class NetworkMode(val configValue: String, @StringRes val labelId: Int) { + FAIR_INPUT_DELAY("fixeddelay", R.string.netplay_network_mode_fair_input_delay), + HOST_INPUT_AUTHORITY("hostinputauthority", R.string.netplay_network_mode_host_input_authority), + GOLF("golf", R.string.netplay_network_mode_golf); + + val isHostInputAuthority: Boolean + get() = this == HOST_INPUT_AUTHORITY || this == GOLF + + companion object { + fun fromConfigValue(value: String): NetworkMode = + entries.find { it.configValue == value } ?: FAIR_INPUT_DELAY + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index bae20aee4682..fe38cfe06712 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -60,6 +60,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { gameFiles = viewModel.gameFiles.collectAsState().value, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, + networkMode = viewModel.networkMode.collectAsState().value, + onNetworkModeChanged = viewModel::setNetworkMode, buffer = viewModel.buffer.collectAsState().value, onBufferChanged = viewModel::setBuffer, clientBuffer = viewModel.clientBuffer.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index e68e6ff7d201..3be5cb0618f8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -95,6 +95,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage +import org.dolphinemu.dolphinemu.features.netplay.model.NetworkMode import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState @@ -121,6 +122,8 @@ fun NetplayScreen( onGameSelected: (GameFile) -> Unit, gameFiles: List, hostInputAuthorityEnabled: Boolean, + networkMode: NetworkMode, + onNetworkModeChanged: (NetworkMode) -> Unit, buffer: Int, onBufferChanged: (Int) -> Unit, clientBuffer: Int, @@ -178,6 +181,8 @@ fun NetplayScreen( onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, + networkMode = networkMode, + onNetworkModeChanged = onNetworkModeChanged, buffer = buffer, onBufferChanged = onBufferChanged, clientBuffer = clientBuffer, @@ -201,6 +206,8 @@ fun NetplayScreen( onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, + networkMode = networkMode, + onNetworkModeChanged = onNetworkModeChanged, buffer = buffer, onBufferChanged = onBufferChanged, clientBuffer = clientBuffer, @@ -290,6 +297,8 @@ private fun PortraitContent( onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, + networkMode: NetworkMode, + onNetworkModeChanged: (NetworkMode) -> Unit, buffer: Int, onBufferChanged: (Int) -> Unit, clientBuffer: Int, @@ -324,6 +333,8 @@ private fun PortraitContent( onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, + networkMode = networkMode, + onNetworkModeChanged = onNetworkModeChanged, buffer = buffer, onBufferChanged = onBufferChanged, clientBuffer = clientBuffer, @@ -356,6 +367,8 @@ private fun LandscapeContent( onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, + networkMode: NetworkMode, + onNetworkModeChanged: (NetworkMode) -> Unit, buffer: Int, onBufferChanged: (Int) -> Unit, clientBuffer: Int, @@ -394,6 +407,8 @@ private fun LandscapeContent( onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, + networkMode = networkMode, + onNetworkModeChanged = onNetworkModeChanged, buffer = buffer, onBufferChanged = onBufferChanged, clientBuffer = clientBuffer, @@ -421,6 +436,8 @@ private fun PlayersAndSettings( onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, + networkMode: NetworkMode, + onNetworkModeChanged: (NetworkMode) -> Unit, buffer: Int, onBufferChanged: (Int) -> Unit, clientBuffer: Int, @@ -475,6 +492,15 @@ private fun PlayersAndSettings( ) } + if (isHosting) { + MenuSpacer() + + NetworkModeDropdown( + networkMode = networkMode, + onNetworkModeChanged = onNetworkModeChanged, + ) + } + if (isHosting && !hostInputAuthorityEnabled) { MenuSpacer() @@ -902,6 +928,47 @@ private fun PlayersTable( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun NetworkModeDropdown( + networkMode: NetworkMode, + onNetworkModeChanged: (NetworkMode) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + OutlinedTextField( + value = stringResource(networkMode.labelId), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.netplay_network_mode_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + // No golf mode for now since it requires in game UI. + listOf(NetworkMode.FAIR_INPUT_DELAY, NetworkMode.HOST_INPUT_AUTHORITY).forEach { mode -> + DropdownMenuItem( + text = { Text(stringResource(mode.labelId)) }, + onClick = { + onNetworkModeChanged(mode) + expanded = false + }, + ) + } + } + } +} + @Composable private fun BufferInput( value: Int, @@ -1214,6 +1281,8 @@ private fun PreviewNetplayScreen() { onGameSelected = {}, gameFiles = emptyList(), hostInputAuthorityEnabled = true, + networkMode = NetworkMode.HOST_INPUT_AUTHORITY, + onNetworkModeChanged = {}, buffer = 5, onBufferChanged = {}, clientBuffer = 10, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index 610f45b2a32d..72877429afa0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -116,7 +116,8 @@ enum class StringSetting( ), NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"), NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"), - NETPLAY_GAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Game", ""); + NETPLAY_GAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Game", ""), + NETPLAY_NETWORK_MODE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "NetworkMode", "fixeddelay"); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 09abe850fb7c..6837e951fef9 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1008,6 +1008,10 @@ It can efficiently compress both junk data and encrypted Wii data. Name Ping Mapping + Input mode + Fair Input Delay + Host Input Authority + Golf Mode Buffer Max buffer Netplay connection lost diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index e845f8ec6072..b19e1a3c9db3 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -49,6 +49,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage client->SendChatMessage(GetJString(env, jmessage)); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetHostInputAuthority( + JNIEnv* env, jobject obj, jboolean enable) +{ + if (auto* server = GetServerPointer(env, obj)) + server->SetHostInputAuthority(static_cast(enable)); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustClientPadBufferSize(JNIEnv* env, jobject obj, From 4c0bf2944bedb428dd46db4cb39ea5dd9d5d8c98 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 12 May 2026 12:58:44 -0700 Subject: [PATCH 069/270] Implement DoAllPlayersHaveGame() check --- .../features/netplay/NetplaySession.kt | 4 +++ .../netplay/model/NetplayViewModel.kt | 13 ++++++++ .../features/netplay/ui/NetplayActivity.kt | 5 ++-- .../features/netplay/ui/NetplayScreen.kt | 30 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 2 ++ Source/Android/jni/NetPlay/Netplay.cpp | 9 ++++++ 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 0ba5d409b0ed..f06ec033dcf1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -170,6 +170,8 @@ class NetplaySession( fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile) + fun doAllPlayersHaveGame(): Boolean = nativeDoAllPlayersHaveGame() + fun startGame() = nativeStartGame() fun getPort(): Int = nativeGetPort() @@ -261,6 +263,8 @@ class NetplaySession( private external fun nativeChangeGame(gameFile: GameFile) + private external fun nativeDoAllPlayersHaveGame(): Boolean + private external fun nativeStartGame() private external fun nativeGetPort(): Int diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 9f9b422f4b69..adea1a13a53b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -9,7 +9,9 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn @@ -99,7 +101,18 @@ class NetplayViewModel( } } + private val _notAllPlayersHaveGame = Channel(Channel.CONFLATED) + val notAllPlayersHaveGame = _notAllPlayersHaveGame.receiveAsFlow() + fun startGame() { + if (netplaySession.doAllPlayersHaveGame()) { + netplaySession.startGame() + } else { + _notAllPlayersHaveGame.trySend(Unit) + } + } + + fun confirmStartGame() { netplaySession.startGame() } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index fe38cfe06712..eb9cecec206d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -9,7 +9,6 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle @@ -49,15 +48,17 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { DolphinTheme { NetplayScreen( onBackClicked = { finish() }, + isHosting = viewModel.isHosting, connectionLost = viewModel.connectionLost, fatalTraversalError = viewModel.fatalTraversalError, messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, - isHosting = viewModel.isHosting, onStartGame = viewModel::startGame, onGameSelected = viewModel::changeGame, gameFiles = viewModel.gameFiles.collectAsState().value, + notAllPlayersHaveGame = viewModel.notAllPlayersHaveGame, + onConfirmStartGame = viewModel::confirmStartGame, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, networkMode = viewModel.networkMode.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 3be5cb0618f8..5d94a2946319 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -121,6 +121,8 @@ fun NetplayScreen( onStartGame: () -> Unit, onGameSelected: (GameFile) -> Unit, gameFiles: List, + notAllPlayersHaveGame: Flow, + onConfirmStartGame: () -> Unit, hostInputAuthorityEnabled: Boolean, networkMode: NetworkMode, onNetworkModeChanged: (NetworkMode) -> Unit, @@ -229,6 +231,11 @@ fun NetplayScreen( fatalTraversalError.collect { traversalError = it } } + var showNotAllPlayersHaveGame by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(Unit) { + notAllPlayersHaveGame.collect { showNotAllPlayersHaveGame = true } + } + var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) } if (saveTransferProgress == null) { dismissSaveTransferProgressDialog = false @@ -279,6 +286,27 @@ fun NetplayScreen( onDismiss = { dismissGameDigestDialog = true }, ) } + + showNotAllPlayersHaveGame -> { + AlertDialog( + title = { Text(stringResource(R.string.netplay_start_warning_title)) }, + text = { Text(stringResource(R.string.netplay_start_warning_not_all_players_have_game)) }, + confirmButton = { + TextButton(onClick = { + showNotAllPlayersHaveGame = false + onConfirmStartGame() + }) { + Text(stringResource(R.string.yes)) + } + }, + dismissButton = { + TextButton(onClick = { showNotAllPlayersHaveGame = false }) { + Text(stringResource(R.string.no)) + } + }, + onDismissRequest = { showNotAllPlayersHaveGame = false }, + ) + } } } } @@ -1280,6 +1308,8 @@ private fun PreviewNetplayScreen() { onStartGame = {}, onGameSelected = {}, gameFiles = emptyList(), + notAllPlayersHaveGame = emptyFlow(), + onConfirmStartGame = {}, hostInputAuthorityEnabled = true, networkMode = NetworkMode.HOST_INPUT_AUTHORITY, onNetworkModeChanged = {}, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 6837e951fef9..58bce8c59524 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -997,6 +997,8 @@ It can efficiently compress both junk data and encrypted Wii data. Port Netplay Start + Warning + Not all players have the game. Do you really want to start? Chat Send Game changed to %1$s diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index b19e1a3c9db3..736f4e01090e 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -181,6 +181,15 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeChangeGame( server->ChangeGame(game_file->GetSyncIdentifier(), game_file->GetLongName()); } +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeDoAllPlayersHaveGame( + JNIEnv* env, jobject obj) +{ + if (auto* client = GetClientPointer(env, obj)) + return static_cast(client->DoAllPlayersHaveGame()); + return JNI_TRUE; +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, jobject obj) From df5e7a9767de04d1114f490b10b44d44a9c1c156 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Thu, 7 May 2026 16:35:18 +0200 Subject: [PATCH 070/270] Update compose dependencies Was using 2025 instead of 2026! Fixed some buggy bottom sheet behaviour. Minor tidying of the chat UI in the bottom sheet --- .../features/netplay/ui/NetplayScreen.kt | 47 ++++++++++--------- .../dolphinemu/ui/theme/DolphinTheme.kt | 45 ++++++++++++++++++ Source/Android/gradle/libs.versions.toml | 2 +- 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 5d94a2946319..8150f0b8e7f1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Remove @@ -57,7 +58,6 @@ import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -86,6 +86,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest import kotlinx.coroutines.flow.Flow @@ -105,6 +106,7 @@ import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme import org.dolphinemu.dolphinemu.ui.theme.ReadOnlyTextField +import org.dolphinemu.dolphinemu.ui.theme.rememberSheetState import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.Locale @@ -564,7 +566,13 @@ private fun Chat( fun LazyListScope.messages() { items(messages.size) { index -> - Text(text = messages[index].message(context)) + Text( + text = messages[index].message(context), + style = MaterialTheme.typography.bodyMedium.copy(lineHeight = 18.sp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp) + ) } } @@ -574,14 +582,10 @@ private fun Chat( draftMessage = "" } - val density = LocalDensity.current - val bottomSheetState = remember { - SheetState( - skipPartiallyExpanded = true, - density = density, - initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden, - ) - } + val bottomSheetState = rememberSheetState( + skipPartiallyExpanded = true, + initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden, + ) if (showBottomSheet) { ModalBottomSheet( @@ -594,7 +598,7 @@ private fun Chat( reverseLayout = true, contentPadding = PaddingValues(bottom = 4.dp), modifier = Modifier - .weight(1f) + .weight(1f, fill = false) .padding(horizontal = DolphinTheme.scaffoldPadding) ) { messages() @@ -605,7 +609,7 @@ private fun Chat( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp) + .padding(8.dp) ) { OutlinedTextField( value = draftMessage, @@ -615,11 +619,14 @@ private fun Chat( modifier = Modifier .weight(1f) ) - TextButton( + IconButton( onClick = submitMessage, enabled = draftMessage.isNotBlank(), ) { - Text(stringResource(R.string.netplay_chat_send)) + Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = stringResource(R.string.netplay_chat_send), + ) } } } @@ -652,14 +659,10 @@ private fun GamePicker( onShowGamePickerChanged: (Boolean) -> Unit, isHosting: Boolean, ) { - val density = LocalDensity.current - val bottomSheetState = remember { - SheetState( - skipPartiallyExpanded = true, - density = density, - initialValue = if (showGamePicker) SheetValue.Expanded else SheetValue.Hidden, - ) - } + val bottomSheetState = rememberSheetState( + skipPartiallyExpanded = true, + initialValue = if (showGamePicker) SheetValue.Expanded else SheetValue.Hidden, + ) if (showGamePicker) { ModalBottomSheet( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index c5fa7463469a..37085b06d913 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -21,18 +21,24 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SheetValue.Hidden import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.google.android.material.color.MaterialColors import androidx.appcompat.R as AppCompatR @@ -241,3 +247,42 @@ fun ReadOnlyTextField( } } } + +// A copy-paste of the internal function used in rememberModalBottomSheetState since +// rememberModalBottomSheetState doesn't expose a way to set the initial value. +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun rememberSheetState( + skipPartiallyExpanded: Boolean = false, + confirmValueChange: (SheetValue) -> Boolean = { true }, + initialValue: SheetValue = Hidden, + skipHiddenState: Boolean = false, + positionalThreshold: Dp = 56.dp, + velocityThreshold: Dp = 125.dp, +): SheetState { + val density = LocalDensity.current + val positionalThresholdToPx = { with(density) { positionalThreshold.toPx() } } + val velocityThresholdToPx = { with(density) { velocityThreshold.toPx() } } + return rememberSaveable( + skipPartiallyExpanded, + confirmValueChange, + skipHiddenState, + saver = + SheetState.Saver( + skipPartiallyExpanded = skipPartiallyExpanded, + positionalThreshold = positionalThresholdToPx, + velocityThreshold = velocityThresholdToPx, + confirmValueChange = confirmValueChange, + skipHiddenState = skipHiddenState, + ), + ) { + SheetState( + skipPartiallyExpanded, + positionalThresholdToPx, + velocityThresholdToPx, + initialValue, + confirmValueChange, + skipHiddenState, + ) + } +} diff --git a/Source/Android/gradle/libs.versions.toml b/Source/Android/gradle/libs.versions.toml index f4811334ac3e..0f91ab31b71f 100644 --- a/Source/Android/gradle/libs.versions.toml +++ b/Source/Android/gradle/libs.versions.toml @@ -4,7 +4,7 @@ appcompat = "1.7.1" benchmarkMacroJunit4 = "1.5.0-alpha04" cardview = "1.0.0" coil = "2.7.0" -compose-bom = "2025.04.00" +compose-bom = "2026.04.01" constraintlayout = "2.2.1" coreKtx = "1.18.0" coreSplashscreen = "1.2.0" From 1bc64a0e6e63ba8584e4d919680cedaf06153369 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sun, 17 May 2026 23:06:17 +0200 Subject: [PATCH 071/270] Fix netplay license headers --- .../dolphinemu/dolphinemu/features/netplay/NetplayManager.kt | 1 - .../dolphinemu/dolphinemu/features/netplay/NetplaySession.kt | 1 - .../dolphinemu/features/netplay/model/ConnectionRole.kt | 2 ++ .../dolphinemu/features/netplay/model/ConnectionType.kt | 2 ++ .../dolphinemu/features/netplay/model/GameDigestProgress.kt | 2 ++ .../dolphinemu/features/netplay/model/NetplayMessage.kt | 2 ++ .../dolphinemu/features/netplay/model/NetworkMode.kt | 2 ++ .../org/dolphinemu/dolphinemu/features/netplay/model/Player.kt | 1 - .../dolphinemu/features/netplay/model/SaveTransferProgress.kt | 2 ++ .../dolphinemu/features/netplay/model/TraversalState.kt | 2 ++ .../dolphinemu/features/netplay/ui/NetplaySetupScreen.kt | 2 ++ Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp | 2 +- Source/Android/jni/NetPlay/NetPlayUICallbacks.h | 3 +++ Source/Android/jni/NetPlay/Netplay.cpp | 2 +- 14 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt index eae44e65be4e..32742c7d93a7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt @@ -1,4 +1,3 @@ -// Copyright 2003 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.dolphinemu.dolphinemu.features.netplay diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index f06ec033dcf1..9e530719d9af 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -1,4 +1,3 @@ -// Copyright 2003 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.dolphinemu.dolphinemu.features.netplay diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt index a9f86d5982c9..d1e0fd71d839 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model import androidx.annotation.StringRes diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt index 52b4aa10b4eb..f82018ea3950 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model import androidx.annotation.StringRes diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt index 57a9cae78b23..923f03068aad 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model data class GameDigestProgress( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt index b8291c094bd4..c408d3be9611 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model import android.content.Context diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt index 55d29f5f170e..6c5f53f06bdb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetworkMode.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model import androidx.annotation.StringRes diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt index 70c228d1c906..fecc65c4b5f1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt @@ -1,4 +1,3 @@ -// Copyright 2003 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.dolphinemu.dolphinemu.features.netplay.model diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt index e16cbab8ca01..8265101415e1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model data class SaveTransferProgress( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt index 9b574e4f6327..0c36fdf58a8b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/TraversalState.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + package org.dolphinemu.dolphinemu.features.netplay.model import android.content.Context diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt index 939a39524d12..f7f660638d10 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + @file:OptIn(ExperimentalMaterial3Api::class) package org.dolphinemu.dolphinemu.features.netplay.ui diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index ce9336ff15b1..9f9c40684683 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -1,4 +1,4 @@ -// Copyright 2003 Dolphin Emulator Project +// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index 57eec55230f3..83ea2c06bc81 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -1,3 +1,6 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #pragma once #include diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 736f4e01090e..06212e8c1d43 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -1,4 +1,4 @@ -// Copyright 2003 Dolphin Emulator Project +// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include From 22863b37032e023631257400f9958f1988fdf514 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 18 May 2026 10:20:14 +0200 Subject: [PATCH 072/270] Add @Keep annotation to Player constructor Since it's only instantiated from c++. --- .../org/dolphinemu/dolphinemu/features/netplay/model/Player.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt index fecc65c4b5f1..31d0d0ccff84 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt @@ -2,6 +2,9 @@ package org.dolphinemu.dolphinemu.features.netplay.model +import androidx.annotation.Keep + +@Keep data class Player( val pid: Int, val name: String, From f63b81dd3963b3e25be707c4e1df7fdb24f220ed Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 18 May 2026 10:25:39 +0200 Subject: [PATCH 073/270] Use unique_ptr when creating netplay client / server --- Source/Android/jni/NetPlay/Netplay.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 06212e8c1d43..e98dc287422c 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -119,17 +119,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); } - auto* client = new NetPlay::NetPlayClient( + auto client = std::make_unique( host_ip, host_port, ui, nickname, NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port}); if (!client->IsConnected()) - { - delete client; return 0; - } - return reinterpret_cast(client); + return reinterpret_cast(client.release()); } JNIEXPORT jlong JNICALL @@ -147,15 +144,12 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv const u16 host_port = is_traversal ? Config::Get(Config::NETPLAY_LISTEN_PORT) : Config::Get(Config::NETPLAY_HOST_PORT); - auto* server = new NetPlay::NetPlayServer( + auto server = std::make_unique( host_port, use_upnp, ui, NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port, traversal_port_alt}); if (!server->is_connected) - { - delete server; return 0; - } const std::string network_mode = Config::Get(Config::NETPLAY_NETWORK_MODE); const bool host_input_authority = @@ -163,7 +157,7 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv server->SetHostInputAuthority(host_input_authority); server->AdjustPadBufferSize(Config::Get(Config::NETPLAY_BUFFER_SIZE)); - return reinterpret_cast(server); + return reinterpret_cast(server.release()); } JNIEXPORT void JNICALL From 06cc09cf09d27de28715a68e58fe8731bc311603 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 19 May 2026 10:26:16 +0200 Subject: [PATCH 074/270] Pass immutable games list to NetplayUICallbacks Remove corresponding jni config. Rename Netplay to NetplaySession in jni config to match new kotlin name. --- .../features/netplay/NetplaySession.kt | 10 ++- Source/Android/jni/AndroidCommon/IDCache.cpp | 79 +++++++------------ Source/Android/jni/AndroidCommon/IDCache.h | 5 +- Source/Android/jni/NetPlay/Netplay.cpp | 23 +++--- 4 files changed, 50 insertions(+), 67 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 9e530719d9af..56676c4d3fa9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -30,17 +30,23 @@ import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.services.GameFileCacheManager class NetplaySession( private val onClosed: (NetplaySession) -> Unit, ) { - private var netPlayUICallbacksPointer: Long = nativeCreateUICallbacks() + @Keep + private var netPlayUICallbacksPointer: Long = + nativeCreateUICallbacks(GameFileCacheManager.getGameFiles().value ?: emptyArray()) + @Keep private var netPlayClientPointer: Long = 0 + @Keep private var netPlayServerPointer: Long = 0 + @Keep private var bootSessionDataPointer: Long = 0 private val sessionScope = CoroutineScope(SupervisorJob()) @@ -238,7 +244,7 @@ class NetplaySession( // JNI methods - private external fun nativeCreateUICallbacks(): Long + private external fun nativeCreateUICallbacks(gameFiles: Array): Long private external fun nativeJoin(): Long diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index db05bda5d5c1..a2c0e14392a0 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -25,10 +25,7 @@ static jmethodID s_game_file_constructor; static jclass s_game_file_cache_class; static jfieldID s_game_file_cache_pointer; -static jclass s_game_file_cache_manager_class; -static jfieldID s_game_file_cache_manager_instance; - -static jclass s_netplay_class; +static jclass s_netplay_session_class; static jfieldID s_net_play_ui_callbacks_pointer; static jfieldID s_net_play_client_pointer; static jfieldID s_net_play_server_pointer; @@ -249,19 +246,9 @@ jfieldID GetGameFileCachePointer() return s_game_file_cache_pointer; } -jclass GetGameFileCacheManagerClass() -{ - return s_game_file_cache_manager_class; -} - -jfieldID GetGameFileCacheManagerInstance() +jclass GetNetplaySessionClass() { - return s_game_file_cache_manager_instance; -} - -jclass GetNetplayClass() -{ - return s_netplay_class; + return s_netplay_session_class; } jfieldID GetNetPlayUICallbacksPointer() @@ -774,55 +761,46 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_game_file_cache_pointer = env->GetFieldID(game_file_cache_class, "pointer", "J"); env->DeleteLocalRef(game_file_cache_class); - const jclass game_file_cache_manager_class = - env->FindClass("org/dolphinemu/dolphinemu/services/GameFileCacheManager"); - s_game_file_cache_manager_class = - reinterpret_cast(env->NewGlobalRef(game_file_cache_manager_class)); - s_game_file_cache_manager_instance = env->GetStaticFieldID( - game_file_cache_manager_class, "gameFileCache", - "Lorg/dolphinemu/dolphinemu/model/GameFileCache;"); - env->DeleteLocalRef(game_file_cache_manager_class); - - const jclass netplay_class = + const jclass netplay_session_class = env->FindClass("org/dolphinemu/dolphinemu/features/netplay/NetplaySession"); - s_netplay_class = reinterpret_cast(env->NewGlobalRef(netplay_class)); + s_netplay_session_class = reinterpret_cast(env->NewGlobalRef(netplay_session_class)); s_net_play_ui_callbacks_pointer = - env->GetFieldID(netplay_class, "netPlayUICallbacksPointer", "J"); - s_net_play_client_pointer = env->GetFieldID(netplay_class, "netPlayClientPointer", "J"); - s_net_play_server_pointer = env->GetFieldID(netplay_class, "netPlayServerPointer", "J"); - s_netplay_on_boot_game = env->GetMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); - s_netplay_on_stop_game = env->GetMethodID(netplay_class, "onStopGame", "()V"); - s_netplay_on_connection_lost = env->GetMethodID(netplay_class, "onConnectionLost", "()V"); - s_netplay_on_connection_error = env->GetMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); + env->GetFieldID(netplay_session_class, "netPlayUICallbacksPointer", "J"); + s_net_play_client_pointer = env->GetFieldID(netplay_session_class, "netPlayClientPointer", "J"); + s_net_play_server_pointer = env->GetFieldID(netplay_session_class, "netPlayServerPointer", "J"); + s_netplay_on_boot_game = env->GetMethodID(netplay_session_class, "onBootGame", "(Ljava/lang/String;J)V"); + s_netplay_on_stop_game = env->GetMethodID(netplay_session_class, "onStopGame", "()V"); + s_netplay_on_connection_lost = env->GetMethodID(netplay_session_class, "onConnectionLost", "()V"); + s_netplay_on_connection_error = env->GetMethodID(netplay_session_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = - env->GetMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); + env->GetMethodID(netplay_session_class, "onGameChanged", "(Ljava/lang/String;)V"); s_netplay_on_host_input_authority_changed = - env->GetMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); + env->GetMethodID(netplay_session_class, "onHostInputAuthorityChanged", "(Z)V"); s_netplay_on_pad_buffer_changed = - env->GetMethodID(netplay_class, "onPadBufferChanged", "(I)V"); + env->GetMethodID(netplay_session_class, "onPadBufferChanged", "(I)V"); s_netplay_on_chat_message_received = - env->GetMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); - s_netplay_update = env->GetMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); + env->GetMethodID(netplay_session_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); + s_netplay_update = env->GetMethodID(netplay_session_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); s_netplay_on_show_chunked_progress_dialog = - env->GetMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); + env->GetMethodID(netplay_session_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); s_netplay_on_set_chunked_progress = - env->GetMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); + env->GetMethodID(netplay_session_class, "onSetChunkedProgress", "(IJ)V"); s_netplay_on_hide_chunked_progress_dialog = - env->GetMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); + env->GetMethodID(netplay_session_class, "onHideChunkedProgressDialog", "()V"); s_netplay_on_desync = - env->GetMethodID(netplay_class, "onDesync", "(ILjava/lang/String;)V"); + env->GetMethodID(netplay_session_class, "onDesync", "(ILjava/lang/String;)V"); s_netplay_on_show_game_digest_dialog = - env->GetMethodID(netplay_class, "onShowGameDigestDialog", "(Ljava/lang/String;)V"); + env->GetMethodID(netplay_session_class, "onShowGameDigestDialog", "(Ljava/lang/String;)V"); s_netplay_on_set_game_digest_progress = - env->GetMethodID(netplay_class, "onSetGameDigestProgress", "(II)V"); + env->GetMethodID(netplay_session_class, "onSetGameDigestProgress", "(II)V"); s_netplay_on_set_game_digest_result = - env->GetMethodID(netplay_class, "onSetGameDigestResult", "(ILjava/lang/String;)V"); + env->GetMethodID(netplay_session_class, "onSetGameDigestResult", "(ILjava/lang/String;)V"); s_netplay_on_abort_game_digest = - env->GetMethodID(netplay_class, "onAbortGameDigest", "()V"); + env->GetMethodID(netplay_session_class, "onAbortGameDigest", "()V"); s_netplay_on_traversal_state_changed = env->GetMethodID( - netplay_class, "onTraversalStateChanged", + netplay_session_class, "onTraversalStateChanged", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); - env->DeleteLocalRef(netplay_class); + env->DeleteLocalRef(netplay_session_class); const jclass netplay_player_class = env->FindClass("org/dolphinemu/dolphinemu/features/netplay/model/Player"); @@ -1043,8 +1021,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_game_file_class); env->DeleteGlobalRef(s_game_file_cache_class); - env->DeleteGlobalRef(s_game_file_cache_manager_class); - env->DeleteGlobalRef(s_netplay_class); + env->DeleteGlobalRef(s_netplay_session_class); env->DeleteGlobalRef(s_netplay_player_class); env->DeleteGlobalRef(s_analytics_class); env->DeleteGlobalRef(s_pair_class); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 064b37df4451..43c4f4358541 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -28,10 +28,7 @@ jmethodID GetGameFileConstructor(); jclass GetGameFileCacheClass(); jfieldID GetGameFileCachePointer(); -jclass GetGameFileCacheManagerClass(); -jfieldID GetGameFileCacheManagerInstance(); - -jclass GetNetplayClass(); +jclass GetNetplaySessionClass(); jfieldID GetNetPlayUICallbacksPointer(); jfieldID GetNetPlayClientPointer(); jfieldID GetNetPlayServerPointer(); diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index e98dc287422c..20ebffe42cf8 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -15,7 +15,6 @@ #include "Core/NetPlayClient.h" #include "Core/NetPlayServer.h" #include "UICommon/GameFile.h" -#include "UICommon/GameFileCache.h" #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" @@ -75,17 +74,21 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustServe } JNIEXPORT jlong JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeCreateUICallbacks(JNIEnv* env, - jobject obj) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeCreateUICallbacks( + JNIEnv* env, jobject obj, jobjectArray jgame_files) { - jobject jgame_file_cache = env->GetStaticObjectField( - IDCache::GetGameFileCacheManagerClass(), IDCache::GetGameFileCacheManagerInstance()); - auto* game_file_cache = reinterpret_cast( - env->GetLongField(jgame_file_cache, IDCache::GetGameFileCachePointer())); - std::vector> games; - game_file_cache->ForEach( - [&games](const std::shared_ptr& game) { games.push_back(game); }); + const jsize count = env->GetArrayLength(jgame_files); + games.reserve(count); + for (jsize i = 0; i < count; i++) + { + jobject jgame_file = env->GetObjectArrayElement(jgame_files, i); + auto* game_ptr = reinterpret_cast*>( + env->GetLongField(jgame_file, IDCache::GetGameFilePointer())); + if (game_ptr) + games.push_back(*game_ptr); + env->DeleteLocalRef(jgame_file); + } return reinterpret_cast(new NetPlay::NetPlayUICallbacks(obj, std::move(games))); } From 4d1e74997749bca7744c1bbe78225e76f57a260c Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 18 May 2026 21:31:39 +0200 Subject: [PATCH 075/270] WithSession helper function Handles the repeated boiler plate of using a weak reference to NetplaySession from NetplayUICallbacks --- .../jni/NetPlay/NetPlayUICallbacks.cpp | 321 +++++++----------- .../Android/jni/NetPlay/NetPlayUICallbacks.h | 12 + 2 files changed, 129 insertions(+), 204 deletions(-) diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 9f9c40684683..de119b43c836 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -68,17 +68,12 @@ NetPlayUICallbacks::NetPlayUICallbacks(jobject netplay_session, if ((state == Core::State::Uninitialized || state == Core::State::Stopping) && !m_got_stop_request) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - auto* client = reinterpret_cast( - env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); - if (client) - client->RequestStopGame(); - - env->DeleteLocalRef(netplay_session); + WithSession([](JNIEnv* env, jobject session) { + auto* client = reinterpret_cast( + env->GetLongField(session, IDCache::GetNetPlayClientPointer())); + if (client) + client->RequestStopGame(); + }); } }); } @@ -99,14 +94,10 @@ void NetPlayUICallbacks::BootGame(const std::string& filename, { m_got_stop_request = false; - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnBootGame(), ToJString(env, filename), - reinterpret_cast(boot_session_data.release())); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnBootGame(), ToJString(env, filename), + reinterpret_cast(boot_session_data.release())); + }); } void NetPlayUICallbacks::StopGame() @@ -116,13 +107,9 @@ void NetPlayUICallbacks::StopGame() m_got_stop_request = true; - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnStopGame()); - env->DeleteLocalRef(netplay_session); + WithSession([](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnStopGame()); + }); } // Only used by Qt UI code, never by the C++ core. On Android, hosting state @@ -131,56 +118,43 @@ bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; + WithSession([](JNIEnv* env, jobject session) { + auto* client = reinterpret_cast( + env->GetLongField(session, IDCache::GetNetPlayClientPointer())); + if (!client) + return; - auto* client = reinterpret_cast( - env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); - if (!client) - { - env->DeleteLocalRef(netplay_session); - return; - } + const std::vector players = client->GetPlayers(); - const std::vector players = client->GetPlayers(); + jobjectArray player_array = env->NewObjectArray( + static_cast(players.size()), IDCache::GetNetplayPlayerClass(), nullptr); - jobjectArray player_array = - env->NewObjectArray(static_cast(players.size()), IDCache::GetNetplayPlayerClass(), nullptr); - - for (jsize i = 0; i < static_cast(players.size()); i++) - { - const NetPlay::Player* player = players[i]; - const std::string mapping = NetPlay::GetPlayerMappingString( - player->pid, client->GetPadMapping(), client->GetGBAConfig(), client->GetWiimoteMapping()); - jobject player_obj = env->NewObject( - IDCache::GetNetplayPlayerClass(), IDCache::GetNetplayPlayerConstructor(), - static_cast(player->pid), - ToJString(env, player->name), - ToJString(env, player->revision), - static_cast(player->ping), - static_cast(player->IsHost()), - ToJString(env, mapping)); - env->SetObjectArrayElement(player_array, i, player_obj); - env->DeleteLocalRef(player_obj); - } + for (jsize i = 0; i < static_cast(players.size()); i++) + { + const NetPlay::Player* player = players[i]; + const std::string mapping = NetPlay::GetPlayerMappingString( + player->pid, client->GetPadMapping(), client->GetGBAConfig(), + client->GetWiimoteMapping()); + jobject player_obj = env->NewObject( + IDCache::GetNetplayPlayerClass(), IDCache::GetNetplayPlayerConstructor(), + static_cast(player->pid), ToJString(env, player->name), + ToJString(env, player->revision), static_cast(player->ping), + static_cast(player->IsHost()), ToJString(env, mapping)); + env->SetObjectArrayElement(player_array, i, player_obj); + env->DeleteLocalRef(player_obj); + } - env->CallVoidMethod(netplay_session, IDCache::GetNetplayUpdate(), player_array); - env->DeleteLocalRef(player_array); - env->DeleteLocalRef(netplay_session); + env->CallVoidMethod(session, IDCache::GetNetplayUpdate(), player_array); + env->DeleteLocalRef(player_array); + }); } void NetPlayUICallbacks::AppendChat(const std::string& message) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnChatMessageReceived(), - ToJString(env, message)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnChatMessageReceived(), + ToJString(env, message)); + }); } void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, @@ -189,34 +163,25 @@ void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_ide m_current_game_identifier = sync_identifier; m_current_game_name = netplay_name; - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnGameChanged(), - ToJString(env, netplay_name)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnGameChanged(), + ToJString(env, netplay_name)); + }); } void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} void NetPlayUICallbacks::OnMsgStartGame() { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - auto* client = reinterpret_cast( - env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); - if (client) - { - if (const auto game = FindGameFile(m_current_game_identifier)) - client->StartGame(game->GetFilePath()); - } - - env->DeleteLocalRef(netplay_session); + WithSession([this](JNIEnv* env, jobject session) { + auto* client = reinterpret_cast( + env->GetLongField(session, IDCache::GetNetPlayClientPointer())); + if (client) + { + if (const auto game = FindGameFile(m_current_game_identifier)) + client->StartGame(game->GetFilePath()); + } + }); } void NetPlayUICallbacks::OnMsgStopGame() {} @@ -232,61 +197,41 @@ void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnPadBufferChanged(), - static_cast(buffer)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnPadBufferChanged(), + static_cast(buffer)); + }); } void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHostInputAuthorityChanged(), - static_cast(enabled)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnHostInputAuthorityChanged(), + static_cast(enabled)); + }); } void NetPlayUICallbacks::OnDesync(u32 frame, const std::string& player) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnDesync(), - static_cast(frame), ToJString(env, player)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnDesync(), + static_cast(frame), ToJString(env, player)); + }); } void NetPlayUICallbacks::OnConnectionLost() { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnConnectionLost()); - env->DeleteLocalRef(netplay_session); + WithSession([](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnConnectionLost()); + }); } void NetPlayUICallbacks::OnConnectionError(const std::string& message) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnConnectionError(), - ToJString(env, message)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnConnectionError(), + ToJString(env, message)); + }); } // No-op — all error info is captured by OnTraversalStateChanged which always fires alongside. @@ -294,17 +239,14 @@ void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State state) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; + WithSession([&](JNIEnv* env, jobject session) { + if (!Common::g_TraversalClient) + return; - jstring host_code = nullptr; - jstring external_address = nullptr; - jstring failure_reason = nullptr; + jstring host_code = nullptr; + jstring external_address = nullptr; + jstring failure_reason = nullptr; - if (Common::g_TraversalClient) - { if (state == Common::TraversalClient::State::Connected) { const auto host_id = Common::g_TraversalClient->GetHostID(); @@ -318,17 +260,16 @@ void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State ToJString(env, FailureReasonToString(Common::g_TraversalClient->GetFailureReason())); } - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnTraversalStateChanged(), + env->CallVoidMethod(session, IDCache::GetNetplayOnTraversalStateChanged(), static_cast(state), host_code, external_address, failure_reason); - } - if (host_code) - env->DeleteLocalRef(host_code); - if (external_address) - env->DeleteLocalRef(external_address); - if (failure_reason) - env->DeleteLocalRef(failure_reason); - env->DeleteLocalRef(netplay_session); + if (host_code) + env->DeleteLocalRef(host_code); + if (external_address) + env->DeleteLocalRef(external_address); + if (failure_reason) + env->DeleteLocalRef(failure_reason); + }); } void NetPlayUICallbacks::OnGameStartAborted() {} @@ -365,89 +306,61 @@ std::string NetPlayUICallbacks::FindGBARomPath(const std::array&, std::s void NetPlayUICallbacks::ShowGameDigestDialog(const std::string& title) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnShowGameDigestDialog(), - ToJString(env, title)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnShowGameDigestDialog(), + ToJString(env, title)); + }); } void NetPlayUICallbacks::SetGameDigestProgress(int pid, int progress) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetGameDigestProgress(), - static_cast(pid), static_cast(progress)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnSetGameDigestProgress(), + static_cast(pid), static_cast(progress)); + }); } void NetPlayUICallbacks::SetGameDigestResult(int pid, const std::string& result) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetGameDigestResult(), - static_cast(pid), ToJString(env, result)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnSetGameDigestResult(), + static_cast(pid), ToJString(env, result)); + }); } void NetPlayUICallbacks::AbortGameDigest() { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnAbortGameDigest()); - env->DeleteLocalRef(netplay_session); + WithSession([](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnAbortGameDigest()); + }); } void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 data_size, std::span players) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - jintArray j_players = env->NewIntArray(static_cast(players.size())); - env->SetIntArrayRegion(j_players, 0, static_cast(players.size()), players.data()); + WithSession([&](JNIEnv* env, jobject session) { + jintArray j_players = env->NewIntArray(static_cast(players.size())); + env->SetIntArrayRegion(j_players, 0, static_cast(players.size()), players.data()); - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnShowChunkedProgressDialog(), - ToJString(env, title), static_cast(data_size), j_players); - env->DeleteLocalRef(j_players); - env->DeleteLocalRef(netplay_session); + env->CallVoidMethod(session, IDCache::GetNetplayOnShowChunkedProgressDialog(), + ToJString(env, title), static_cast(data_size), j_players); + env->DeleteLocalRef(j_players); + }); } void NetPlayUICallbacks::HideChunkedProgressDialog() { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHideChunkedProgressDialog()); - env->DeleteLocalRef(netplay_session); + WithSession([](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnHideChunkedProgressDialog()); + }); } void NetPlayUICallbacks::SetChunkedProgress(int pid, u64 progress) { - JNIEnv* env = IDCache::GetEnvForThread(); - jobject netplay_session = GetNetplaySessionLocalRef(env); - if (!netplay_session) - return; - - env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetChunkedProgress(), - static_cast(pid), static_cast(progress)); - env->DeleteLocalRef(netplay_session); + WithSession([&](JNIEnv* env, jobject session) { + env->CallVoidMethod(session, IDCache::GetNetplayOnSetChunkedProgress(), + static_cast(pid), static_cast(progress)); + }); } void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) {} diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index 83ea2c06bc81..602512e3b8d3 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -13,6 +13,7 @@ #include "Common/HookableEvent.h" #include "Core/NetPlayClient.h" #include "UICommon/GameFile.h" +#include "jni/AndroidCommon/IDCache.h" namespace NetPlay { @@ -67,6 +68,17 @@ class NetPlayUICallbacks : public NetPlay::NetPlayUI { private: jobject GetNetplaySessionLocalRef(JNIEnv* env) const; + template + void WithSession(F&& callback) const + { + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + callback(env, netplay_session); + env->DeleteLocalRef(netplay_session); + } + jweak m_netplay_session; std::vector> m_games; NetPlay::SyncIdentifier m_current_game_identifier; From 8d565e0331cf198dddc6d26d8308f8f582e80b13 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 18 May 2026 21:35:13 +0200 Subject: [PATCH 076/270] Use the dual pane netplay screen layout only if the width is sufficient To avoid using dual pane layout on very small devices, ensure that the current width available for display is at least medium (600dp). It also has to be landscape, the dual pane mode looks weird on a tablet in portrait mode since there's not that much content. --- Source/Android/app/build.gradle.kts | 1 + .../dolphinemu/features/netplay/ui/NetplayScreen.kt | 10 +++++++++- Source/Android/gradle/libs.versions.toml | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 27f76b22a2d3..11433e81604f 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -166,6 +166,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material.icons) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.ui) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 8150f0b8e7f1..34958176f760 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -61,6 +61,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -87,6 +88,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.window.core.layout.WindowSizeClass import coil.compose.AsyncImage import coil.request.ImageRequest import kotlinx.coroutines.flow.Flow @@ -171,7 +173,13 @@ fun NetplayScreen( mutableStateOf(joinAddresses.keys.firstOrNull() ?: JoinInfoType.EXTERNAL) } - if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { + val isLandscape = + LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + val isWidthAtLeastMedium = currentWindowAdaptiveInfo().windowSizeClass + .isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) + val useWideLayout = isLandscape && isWidthAtLeastMedium + + if (useWideLayout) { LandscapeContent( isHosting = isHosting, messages = messages, diff --git a/Source/Android/gradle/libs.versions.toml b/Source/Android/gradle/libs.versions.toml index 0f91ab31b71f..f9c4992ccdfa 100644 --- a/Source/Android/gradle/libs.versions.toml +++ b/Source/Android/gradle/libs.versions.toml @@ -35,6 +35,7 @@ androidx-cardview = { group = "androidx.cardview", name = "cardview", version.re androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" } androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } From fde035dc0c716e86173478fb30751f349a168e1e Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 19 May 2026 08:46:16 +0200 Subject: [PATCH 077/270] Replace hardcoded save progress string --- .../dolphinemu/features/netplay/ui/NetplayScreen.kt | 6 +++++- Source/Android/app/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 34958176f760..c4831391a5ab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -1165,7 +1165,11 @@ private fun SaveTransferProgressRow( ) Spacer(modifier = Modifier.width(16.dp)) Text( - text = "${formatMib(playerProgress.progress)}/${formatMib(totalSize)} MiB", + text = stringResource( + R.string.netplay_transfer_progress, + formatMib(playerProgress.progress), + formatMib(totalSize) + ), ) } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 58bce8c59524..fd72abd12b1d 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1035,4 +1035,5 @@ It can efficiently compress both junk data and encrypted Wii data. Unknown Share address Retry + %1$s/%2$s MiB From 5b49060a9d8f091cc4d1ebe967e01fb3e0d67515 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 19 May 2026 09:46:22 +0200 Subject: [PATCH 078/270] Fix NetplayScreen edge to edge NetplayScreen was not drawing under the bottom nav bar like other screens. In landscape mode half of the screen scrolls and half is fixed, to make this look natural with edge to edge, provide a fade on the bottom of the scrolling side so things look natural when the screens loads and is resting at 0 scroll offset. --- .../features/netplay/ui/NetplayScreen.kt | 24 +++++++-- .../dolphinemu/ui/theme/DolphinTheme.kt | 49 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index c4831391a5ab..54ea403eeb9d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -108,6 +108,7 @@ import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme import org.dolphinemu.dolphinemu.ui.theme.ReadOnlyTextField +import org.dolphinemu.dolphinemu.ui.theme.bottomFadeOverlay import org.dolphinemu.dolphinemu.ui.theme.rememberSheetState import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.Locale @@ -164,7 +165,6 @@ fun NetplayScreen( val modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding) - .padding(innerPadding) // State which must live above the landscape/portrait split. var showChat by rememberSaveable { mutableStateOf(false) } @@ -202,6 +202,7 @@ fun NetplayScreen( joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, + contentPadding = innerPadding, modifier = modifier ) } else { @@ -227,6 +228,7 @@ fun NetplayScreen( joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, + contentPadding = innerPadding, modifier = modifier ) } @@ -344,11 +346,13 @@ private fun PortraitContent( joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, + contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { Column( modifier = modifier .verticalScroll(rememberScrollState()) + .padding(contentPadding) ) { Chat( messages = messages, @@ -414,11 +418,11 @@ private fun LandscapeContent( joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, + contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { Row( modifier = modifier - .padding(horizontal = DolphinTheme.scaffoldPadding) ) { Chat( messages = messages, @@ -426,16 +430,26 @@ private fun LandscapeContent( showBottomSheet = showChat, onShowBottomSheetChanged = onShowChatChanged, modifier = Modifier + .padding(contentPadding) + .padding( + start = DolphinTheme.scaffoldPadding, + end = DolphinTheme.scaffoldPadding / 2, + ) .weight(1f) .fillMaxHeight() ) - Spacer(modifier = Modifier.width(16.dp)) - + val scrollState = rememberScrollState() Column( modifier = Modifier .weight(1f) - .verticalScroll(rememberScrollState()) + .bottomFadeOverlay(scrollState, contentPadding.calculateBottomPadding()) + .verticalScroll(scrollState) + .padding(contentPadding) + .padding( + start = DolphinTheme.scaffoldPadding / 2, + end = DolphinTheme.scaffoldPadding + ) ) { PlayersAndSettings( game = game, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index 37085b06d913..c142a44d73db 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -5,11 +5,13 @@ package org.dolphinemu.dolphinemu.ui.theme import android.content.Context import androidx.annotation.AttrRes import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -28,10 +30,15 @@ import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -286,3 +293,45 @@ fun rememberSheetState( ) } } + +@Composable +fun Modifier.bottomFadeOverlay( + scrollState: ScrollState, + fadeHeight: Dp, +): Modifier { + val surfaceColor = MaterialTheme.colorScheme.surface + val bottomPaddingPx = with(LocalDensity.current) { + fadeHeight.toPx() + } + val fadeDelayPx = with(LocalDensity.current) { 8.dp.toPx() } + val fadeDurationPx = with(LocalDensity.current) { 48.dp.toPx() } + val scrollProgress by remember { + derivedStateOf { + if (bottomPaddingPx <= 0f) 1f + else ((scrollState.value.coerceAtLeast(0) - fadeDelayPx) / fadeDurationPx) + .coerceIn(0f, 1f) + } + } + val surfaceStopFraction = (scrollProgress * 2f).coerceIn(0f, 1f) + val transparentStopFraction = ((scrollProgress * 2f) - 1f).coerceIn(0f, 1f) + + return drawWithContent { + drawContent() + if (transparentStopFraction < 1f) { + drawRect( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0f to Color.Transparent, + transparentStopFraction to Color.Transparent, + surfaceStopFraction to surfaceColor, + 1f to surfaceColor, + ), + startY = size.height - bottomPaddingPx, + endY = size.height, + ), + topLeft = Offset(0f, size.height - bottomPaddingPx), + size = Size(size.width, bottomPaddingPx), + ) + } + } +} From 1d1c9696aeee312e003d200fde105ab9da6a409c Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 19 May 2026 09:56:49 +0200 Subject: [PATCH 079/270] lint formatting --- Source/Android/jni/AndroidCommon/IDCache.cpp | 24 ++-- Source/Android/jni/MainAndroid.cpp | 9 +- .../jni/NetPlay/NetPlayUICallbacks.cpp | 108 +++++++++++------- .../Android/jni/NetPlay/NetPlayUICallbacks.h | 6 +- Source/Android/jni/NetPlay/Netplay.cpp | 57 +++++---- .../Core/DolphinQt/NetPlay/NetPlayDialog.cpp | 2 +- 6 files changed, 121 insertions(+), 85 deletions(-) diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index a2c0e14392a0..58cec0230890 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -768,10 +768,12 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetFieldID(netplay_session_class, "netPlayUICallbacksPointer", "J"); s_net_play_client_pointer = env->GetFieldID(netplay_session_class, "netPlayClientPointer", "J"); s_net_play_server_pointer = env->GetFieldID(netplay_session_class, "netPlayServerPointer", "J"); - s_netplay_on_boot_game = env->GetMethodID(netplay_session_class, "onBootGame", "(Ljava/lang/String;J)V"); + s_netplay_on_boot_game = + env->GetMethodID(netplay_session_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_stop_game = env->GetMethodID(netplay_session_class, "onStopGame", "()V"); s_netplay_on_connection_lost = env->GetMethodID(netplay_session_class, "onConnectionLost", "()V"); - s_netplay_on_connection_error = env->GetMethodID(netplay_session_class, "onConnectionError", "(Ljava/lang/String;)V"); + s_netplay_on_connection_error = + env->GetMethodID(netplay_session_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = env->GetMethodID(netplay_session_class, "onGameChanged", "(Ljava/lang/String;)V"); s_netplay_on_host_input_authority_changed = @@ -780,9 +782,11 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(netplay_session_class, "onPadBufferChanged", "(I)V"); s_netplay_on_chat_message_received = env->GetMethodID(netplay_session_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); - s_netplay_update = env->GetMethodID(netplay_session_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); - s_netplay_on_show_chunked_progress_dialog = - env->GetMethodID(netplay_session_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); + s_netplay_update = + env->GetMethodID(netplay_session_class, "onUpdate", + "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); + s_netplay_on_show_chunked_progress_dialog = env->GetMethodID( + netplay_session_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); s_netplay_on_set_chunked_progress = env->GetMethodID(netplay_session_class, "onSetChunkedProgress", "(IJ)V"); s_netplay_on_hide_chunked_progress_dialog = @@ -797,15 +801,17 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(netplay_session_class, "onSetGameDigestResult", "(ILjava/lang/String;)V"); s_netplay_on_abort_game_digest = env->GetMethodID(netplay_session_class, "onAbortGameDigest", "()V"); - s_netplay_on_traversal_state_changed = env->GetMethodID( - netplay_session_class, "onTraversalStateChanged", - "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + s_netplay_on_traversal_state_changed = + env->GetMethodID(netplay_session_class, "onTraversalStateChanged", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); env->DeleteLocalRef(netplay_session_class); const jclass netplay_player_class = env->FindClass("org/dolphinemu/dolphinemu/features/netplay/model/Player"); s_netplay_player_class = reinterpret_cast(env->NewGlobalRef(netplay_player_class)); - s_netplay_player_constructor = env->GetMethodID(netplay_player_class, "", "(ILjava/lang/String;Ljava/lang/String;IZLjava/lang/String;)V"); + s_netplay_player_constructor = + env->GetMethodID(netplay_player_class, "", + "(ILjava/lang/String;Ljava/lang/String;IZLjava/lang/String;)V"); env->DeleteLocalRef(netplay_player_class); const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics"); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 0d29ad390861..bbf65230f29f 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -35,7 +35,6 @@ #include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" -#include "jni/NetPlay/NetPlayUICallbacks.h" #include "Core/BootManager.h" #include "Core/CommonTitles.h" #include "Core/ConfigLoaders/GameConfigLoader.h" @@ -50,6 +49,7 @@ #include "Core/PowerPC/PowerPC.h" #include "Core/State.h" #include "Core/System.h" +#include "jni/NetPlay/NetPlayUICallbacks.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" @@ -620,13 +620,14 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_Run___3Ljava_lang_String_2ZLjava_la JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunNetPlay( JNIEnv* env, jclass, jobjectArray jPaths, jboolean jRiivolution, jlong jBootSessionData) { - auto boot_session_data = std::unique_ptr( - reinterpret_cast(jBootSessionData)); + auto boot_session_data = + std::unique_ptr(reinterpret_cast(jBootSessionData)); if (!boot_session_data) { env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetDisplayToastMsg(), ToJString(env, "Netplay: no boot session data"), JNI_TRUE); - env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetFinishEmulationActivity()); + env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetFinishEmulationActivity()); return; } Run(env, JStringArrayToVector(env, jPaths), jRiivolution, std::move(*boot_session_data)); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index de119b43c836..4b61495f7f3b 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -3,13 +3,13 @@ #include -#include "UICommon/GameFile.h" -#include "UICommon/UICommon.h" -#include "NetPlayUICallbacks.h" #include "Common/TraversalClient.h" #include "Core/Boot/Boot.h" #include "Core/Core.h" #include "Core/System.h" +#include "NetPlayUICallbacks.h" +#include "UICommon/GameFile.h" +#include "UICommon/UICommon.h" #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" @@ -57,7 +57,8 @@ const char* FailureReasonToString(Common::TraversalClient::FailureReason reason) } } // namespace -namespace NetPlay { +namespace NetPlay +{ NetPlayUICallbacks::NetPlayUICallbacks(jobject netplay_session, std::vector> games) @@ -114,7 +115,10 @@ void NetPlayUICallbacks::StopGame() // Only used by Qt UI code, never by the C++ core. On Android, hosting state // is tracked in Kotlin (NetplaySession.isHosting). -bool NetPlayUICallbacks::IsHosting() const { return false; } +bool NetPlayUICallbacks::IsHosting() const +{ + return false; +} void NetPlayUICallbacks::Update() { @@ -126,20 +130,20 @@ void NetPlayUICallbacks::Update() const std::vector players = client->GetPlayers(); - jobjectArray player_array = env->NewObjectArray( - static_cast(players.size()), IDCache::GetNetplayPlayerClass(), nullptr); + jobjectArray player_array = env->NewObjectArray(static_cast(players.size()), + IDCache::GetNetplayPlayerClass(), nullptr); for (jsize i = 0; i < static_cast(players.size()); i++) { const NetPlay::Player* player = players[i]; - const std::string mapping = NetPlay::GetPlayerMappingString( - player->pid, client->GetPadMapping(), client->GetGBAConfig(), - client->GetWiimoteMapping()); - jobject player_obj = env->NewObject( - IDCache::GetNetplayPlayerClass(), IDCache::GetNetplayPlayerConstructor(), - static_cast(player->pid), ToJString(env, player->name), - ToJString(env, player->revision), static_cast(player->ping), - static_cast(player->IsHost()), ToJString(env, mapping)); + const std::string mapping = + NetPlay::GetPlayerMappingString(player->pid, client->GetPadMapping(), + client->GetGBAConfig(), client->GetWiimoteMapping()); + jobject player_obj = + env->NewObject(IDCache::GetNetplayPlayerClass(), IDCache::GetNetplayPlayerConstructor(), + static_cast(player->pid), ToJString(env, player->name), + ToJString(env, player->revision), static_cast(player->ping), + static_cast(player->IsHost()), ToJString(env, mapping)); env->SetObjectArrayElement(player_array, i, player_obj); env->DeleteLocalRef(player_obj); } @@ -164,12 +168,13 @@ void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_ide m_current_game_name = netplay_name; WithSession([&](JNIEnv* env, jobject session) { - env->CallVoidMethod(session, IDCache::GetNetplayOnGameChanged(), - ToJString(env, netplay_name)); + env->CallVoidMethod(session, IDCache::GetNetplayOnGameChanged(), ToJString(env, netplay_name)); }); } -void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} +void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) +{ +} void NetPlayUICallbacks::OnMsgStartGame() { @@ -184,7 +189,9 @@ void NetPlayUICallbacks::OnMsgStartGame() }); } -void NetPlayUICallbacks::OnMsgStopGame() {} +void NetPlayUICallbacks::OnMsgStopGame() +{ +} void NetPlayUICallbacks::OnMsgPowerButton() { @@ -192,8 +199,12 @@ void NetPlayUICallbacks::OnMsgPowerButton() UICommon::TriggerSTMPowerEvent(); } -void NetPlayUICallbacks::OnPlayerConnect(const std::string&) {} -void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} +void NetPlayUICallbacks::OnPlayerConnect(const std::string&) +{ +} +void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) +{ +} void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) { @@ -214,8 +225,8 @@ void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) void NetPlayUICallbacks::OnDesync(u32 frame, const std::string& player) { WithSession([&](JNIEnv* env, jobject session) { - env->CallVoidMethod(session, IDCache::GetNetplayOnDesync(), - static_cast(frame), ToJString(env, player)); + env->CallVoidMethod(session, IDCache::GetNetplayOnDesync(), static_cast(frame), + ToJString(env, player)); }); } @@ -229,13 +240,14 @@ void NetPlayUICallbacks::OnConnectionLost() void NetPlayUICallbacks::OnConnectionError(const std::string& message) { WithSession([&](JNIEnv* env, jobject session) { - env->CallVoidMethod(session, IDCache::GetNetplayOnConnectionError(), - ToJString(env, message)); + env->CallVoidMethod(session, IDCache::GetNetplayOnConnectionError(), ToJString(env, message)); }); } // No-op — all error info is captured by OnTraversalStateChanged which always fires alongside. -void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {} +void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) +{ +} void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State state) { @@ -272,12 +284,25 @@ void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State }); } -void NetPlayUICallbacks::OnGameStartAborted() {} -void NetPlayUICallbacks::OnGolferChanged(bool, const std::string&) {} -void NetPlayUICallbacks::OnTtlDetermined(u8) {} -void NetPlayUICallbacks::OnIndexAdded(bool, std::string) {} -void NetPlayUICallbacks::OnIndexRefreshFailed(std::string) {} -bool NetPlayUICallbacks::IsRecording() { return false; } +void NetPlayUICallbacks::OnGameStartAborted() +{ +} +void NetPlayUICallbacks::OnGolferChanged(bool, const std::string&) +{ +} +void NetPlayUICallbacks::OnTtlDetermined(u8) +{ +} +void NetPlayUICallbacks::OnIndexAdded(bool, std::string) +{ +} +void NetPlayUICallbacks::OnIndexRefreshFailed(std::string) +{ +} +bool NetPlayUICallbacks::IsRecording() +{ + return false; +} std::shared_ptr NetPlayUICallbacks::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, @@ -302,7 +327,10 @@ NetPlayUICallbacks::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier, return result; } -std::string NetPlayUICallbacks::FindGBARomPath(const std::array&, std::string_view, int) { return {}; } +std::string NetPlayUICallbacks::FindGBARomPath(const std::array&, std::string_view, int) +{ + return {}; +} void NetPlayUICallbacks::ShowGameDigestDialog(const std::string& title) { @@ -323,8 +351,8 @@ void NetPlayUICallbacks::SetGameDigestProgress(int pid, int progress) void NetPlayUICallbacks::SetGameDigestResult(int pid, const std::string& result) { WithSession([&](JNIEnv* env, jobject session) { - env->CallVoidMethod(session, IDCache::GetNetplayOnSetGameDigestResult(), - static_cast(pid), ToJString(env, result)); + env->CallVoidMethod(session, IDCache::GetNetplayOnSetGameDigestResult(), static_cast(pid), + ToJString(env, result)); }); } @@ -336,7 +364,7 @@ void NetPlayUICallbacks::AbortGameDigest() } void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 data_size, - std::span players) + std::span players) { WithSession([&](JNIEnv* env, jobject session) { jintArray j_players = env->NewIntArray(static_cast(players.size())); @@ -358,11 +386,13 @@ void NetPlayUICallbacks::HideChunkedProgressDialog() void NetPlayUICallbacks::SetChunkedProgress(int pid, u64 progress) { WithSession([&](JNIEnv* env, jobject session) { - env->CallVoidMethod(session, IDCache::GetNetplayOnSetChunkedProgress(), - static_cast(pid), static_cast(progress)); + env->CallVoidMethod(session, IDCache::GetNetplayOnSetChunkedProgress(), static_cast(pid), + static_cast(progress)); }); } -void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) {} +void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) +{ +} } // namespace NetPlay diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index 602512e3b8d3..946948d2db86 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -15,9 +15,11 @@ #include "UICommon/GameFile.h" #include "jni/AndroidCommon/IDCache.h" -namespace NetPlay { +namespace NetPlay +{ -class NetPlayUICallbacks : public NetPlay::NetPlayUI { +class NetPlayUICallbacks : public NetPlay::NetPlayUI +{ public: NetPlayUICallbacks(jobject netplay_session, std::vector> games); diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 20ebffe42cf8..d6e24d250b05 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -9,10 +9,10 @@ #include "Common/CommonTypes.h" #include "Common/TraversalClient.h" -#include "Core/NetPlayCommon.h" #include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" +#include "Core/NetPlayCommon.h" #include "Core/NetPlayServer.h" #include "UICommon/GameFile.h" @@ -22,8 +22,8 @@ static NetPlay::NetPlayUICallbacks* GetUICallbacksPointer(JNIEnv* env, jobject obj) { - return reinterpret_cast( - env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); } static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) @@ -41,8 +41,9 @@ static NetPlay::NetPlayServer* GetServerPointer(JNIEnv* env, jobject obj) extern "C" { JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage(JNIEnv* env, jobject obj, - jstring jmessage) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage(JNIEnv* env, + jobject obj, + jstring jmessage) { if (auto* client = GetClientPointer(env, obj)) client->SendChatMessage(GetJString(env, jmessage)); @@ -50,16 +51,15 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetHostInputAuthority( - JNIEnv* env, jobject obj, jboolean enable) + JNIEnv* env, jobject obj, jboolean enable) { - if (auto* server = GetServerPointer(env, obj)) - server->SetHostInputAuthority(static_cast(enable)); + if (auto* server = GetServerPointer(env, obj)) + server->SetHostInputAuthority(static_cast(enable)); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustClientPadBufferSize(JNIEnv* env, - jobject obj, - jint buffer) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustClientPadBufferSize( + JNIEnv* env, jobject obj, jint buffer) { if (auto* client = GetClientPointer(env, obj)) client->AdjustPadBufferSize(static_cast(buffer)); @@ -118,7 +118,7 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); is_traversal = traversal_choice == "traversal"; host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : - Config::Get(Config::NETPLAY_ADDRESS); + Config::Get(Config::NETPLAY_ADDRESS); host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); } @@ -144,19 +144,19 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); const u16 traversal_port_alt = Config::Get(Config::NETPLAY_TRAVERSAL_PORT_ALT); - const u16 host_port = is_traversal ? Config::Get(Config::NETPLAY_LISTEN_PORT) - : Config::Get(Config::NETPLAY_HOST_PORT); + const u16 host_port = is_traversal ? Config::Get(Config::NETPLAY_LISTEN_PORT) : + Config::Get(Config::NETPLAY_HOST_PORT); auto server = std::make_unique( host_port, use_upnp, ui, - NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port, traversal_port_alt}); + NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port, + traversal_port_alt}); if (!server->is_connected) return 0; const std::string network_mode = Config::Get(Config::NETPLAY_NETWORK_MODE); - const bool host_input_authority = - network_mode == "hostinputauthority" || network_mode == "golf"; + const bool host_input_authority = network_mode == "hostinputauthority" || network_mode == "golf"; server->SetHostInputAuthority(host_input_authority); server->AdjustPadBufferSize(Config::Get(Config::NETPLAY_BUFFER_SIZE)); @@ -189,7 +189,7 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeDoAllPlayer JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, - jobject obj) + jobject obj) { auto* server = GetServerPointer(env, obj); if (!server) @@ -198,9 +198,8 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(J server->RequestStartGame(); } -JNIEXPORT jint JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPort(JNIEnv* env, - jobject obj) +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPort( + JNIEnv* env, jobject obj) { if (auto* server = GetServerPointer(env, obj)) return static_cast(server->GetPort()); @@ -219,38 +218,36 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetExternal JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReconnectTraversal(JNIEnv*, - jobject) + jobject) { if (Common::g_TraversalClient) Common::g_TraversalClient->ReconnectToServer(); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, - jobject, - jlong pointer) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks( + JNIEnv*, jobject, jlong pointer) { delete reinterpret_cast(pointer); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseBootSessionData(JNIEnv*, - jobject, - jlong pointer) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseBootSessionData( + JNIEnv*, jobject, jlong pointer) { delete reinterpret_cast(pointer); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseClient(JNIEnv*, jobject, - jlong pointer) + jlong pointer) { delete reinterpret_cast(pointer); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseServer(JNIEnv*, jobject, - jlong pointer) + jlong pointer) { delete reinterpret_cast(pointer); } diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index 5282fdb9b99d..733ba76bd10b 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -30,9 +30,9 @@ #endif #include "Common/Config/Config.h" -#include "Core/NetPlayCommon.h" #include "Common/Logging/Log.h" #include "Common/TraversalClient.h" +#include "Core/NetPlayCommon.h" #include "Core/Boot/Boot.h" #include "Core/Config/GraphicsSettings.h" From 915b83959821e12ec3f0f440cb72a40d91f3c9af Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Sat, 23 May 2026 22:39:42 -0400 Subject: [PATCH 080/270] Approve Xenoblade Chronicles 60 FPS Gecko code --- Data/Sys/ApprovedInis.json | 2 +- Data/Sys/GameSettings/SX4E01.ini | 230 +++++++++++++++++++++ Source/Core/Core/AchievementApprovedHash.h | 4 +- 3 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 Data/Sys/GameSettings/SX4E01.ini diff --git a/Data/Sys/ApprovedInis.json b/Data/Sys/ApprovedInis.json index 45749af8ca41..2642012deda7 100644 --- a/Data/Sys/ApprovedInis.json +++ b/Data/Sys/ApprovedInis.json @@ -1 +1 @@ -{"D43J01":{"loophack":"CAB9CED2D904F12CCB21F5B1DE9B5433620C3E13"},"DPOJ8P":{"Bypass Modem Detection":"90BA66E25640A538FEFA6693715718139079FA7B"},"G2BE5G":{"Disable interlaced rendering":"7FFF6BDD93713BEDFD23739C32B86153FA19AEA0"},"G2BP7D":{"Disable interlaced rendering":"56E85D7285F10348E1E5354E379918D07E79EDA9"},"G2VE08":{"16:9 Widescreen":"3ED033396382424533A263A39687A0961EC994A6"},"G2VP08":{"16:9 Widescreen":"EEBC3C5F04BB70AFCA1A4C3D638335CDC9142BA2"},"G3RP52":{"16:9 Widescreen":"7F6B7743F0DDD3704B15882330E490103AE24E36","60fps":"4C9A8572A35F1DB85BBB2F32ED34288A343F1FC0"},"G3YP52":{"16:9 Widescreen":"3D102AFEE2F4945894E74BB702ED3ED2294C6FEC","Force PAL60":"FD61581C593EA34DD358AE33819FAB310F2CE961"},"G4AEE9":{"16:9 Aspect Ratio Fix":"8C2564CF47BB6A9B57D712C90E17F459B0BB6CCD"},"G4ME69":{"16:9 Widescreen":"6FC6E5D91693FAABE25CC721F2121CB0B5CA9D12"},"G5SE7D":{"16:9 Widescreen":"16119398BFE241A84151C046E0BC5BCF89D22049","Remove Bloom":"A5F2FB2B3BFF61F34F8AB1DD5A912A0FD8DFA720"},"G5SP7D":{"16:9 Widescreen":"718AA8BD831961C33114852FDA75BB58F768F46A"},"G8ME01":{"16:9 Aspect Ratio Fix - Centered HUD":"8C2FC3FC6F7766AA8E87425BF3BDD2E46E73DE71","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"AE3DBE2604CA0660165CA870C4B80E2B523C9475","16:9 Aspect Ratio Fix - Normal HUD":"F6E8264CF14D0189219C1E2D20C4182F190F4B88","16:9 Aspect Ratio Fix - Stretched HUD":"8EA4C7B80ADE3E8FEC28EEFA3A046B6B04162E81","No Letterbox":"B1DE2A3526F2E4511F2B94A5171BF7B3C2C52B9B"},"G8MJ01":{"16:9 Aspect Ratio Fix - Centered HUD":"B3181CA1AAB11EAE9132184DA48D76E775AC0A24","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"904C0EEEF16D761F7BDF7099DEB384DD9AB26E00","16:9 Aspect Ratio Fix - Normal HUD":"631CE99BDE3F1475303C00C67E3C130D4EB2547B","16:9 Aspect Ratio Fix - Stretched HUD":"81F7AD591F12A01DE82850457ECF147998D1238F"},"G8MP01":{"16:9 Aspect Ratio Fix - Centered HUD":"75AF5D7FA49BE9C3C73EA1482B49CD9B36CE817E","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"C2E5003E74551DF693A7CB814850CA51D77549EF","16:9 Aspect Ratio Fix - Normal HUD":"FB26AA84B0CB787705E84E869B649307776A2839","16:9 Aspect Ratio Fix - Stretched HUD":"C5F8A81520193418C07C7BC4881F02BE9C4DC9BC","No Letterbox":"7828F5B0C8B470620755C16C60A5A4554F92640C"},"G9SE8P":{"16:9 Widescreen":"CD043229AC47AC93D537B6A0725A2D0BB6FEF4E8"},"G9SJ8P":{"16:9 Widescreen":"C55EF90B542AA3973352A232629A828BC22A8509"},"G9SP8P":{"16:9 Widescreen":"9521774ADEE769CFCCE86F8170FE626387E73019"},"GAFE01":{"16:9 Widescreen":"D24F6D9BE5D79D3C795C2844634A0214BB68B5DA","Make Game Save Copyable (donny2112)":"F9A812FF62A20D5440CAB79DB6AFAE068457A10E"},"GAHEGG":{"Limit internal frame rate (speed hack)":"2873E5178C8068AFA8AFE751F903A69CA9A41F16"},"GAL":{"Trophy Viewer":"DBDB50216B22CDFFF77BDD80261CD0FF75803D3B"},"GALE01r0":{"Normal C Stick Functionality in Singleplayer Modes":"9DC8D90D986FAC175F45006AFF20CF00EDFBE053","Proper 16:9 Widescreen Support":"18B4159E77013EDFE499A33281B771C442970E3B","Properly Display in 4:3":"DC2BCA7D99EBDE1DC7C68CA6AC7F543B93FD6A67"},"GALE01r1":{"Normal C Stick Functionality in Singleplayer Modes":"F6030959C1CAC92F37287D556F18B3C5298FAD5B"},"GALE01r2":{"Disable Rumble":"CFDEEA8AD2A99394BFFEA7211EF03394E7471FA9","Normal C Stick Functionality in Singleplayer Modes":"BCD918A72368B2A30E797C76994E7539A1C0B5D9","PAL Stock Icons":"6B92D95F0EEA165EA84FD753F74B0A9E2079C2D7","Proper 16:9 Widescreen Support":"CEDF18354CDCFC49398551C629E51B053331A55B","Widescreen Support v1.2 [Dan Salvato]":"D32ED4CD685F3FF649F7E749E8C182C657013918"},"GALP01":{"Normal C Stick Functionality in Singleplayer Modes":"4817F202678559291F9F76B46DC7561A45D00B77","Proper 16:9 Widescreen Support":"BFCBE098FE8679B9140A48EA1D8620D5A369697C","Properly Display in 4:3":"5BA2933B8365E21553AA273B91954A4C722B1035"},"GAZE69":{"60FPS":"BC281053C5266B21FDDC6C8996A6021DEA03D459"},"GBLE52":{"16:9 Widescreen":"C0F3AB3BBC9B21F4BF25F44E48CF1CD36D2B3F45"},"GBLP52":{"16:9 Widescreen":"797040CB097BFD369490A1BE29659929D76CE6F7"},"GBRJ18":{"16:9 Widescreen":"B3C867AB34D90E2E9C5B799D800F4C191DE279FA"},"GBSE8P":{"16:9 Widescreen":"4148D453F61A5B81E53669335BC667E651865C76"},"GBSP8P":{"16:9 Widescreen":"6CB029AE768BA5B9995D5F12C1EBB37B3D1FE242"},"GC6E01":{"16:9 Widescreen":"DE932CCAD1B5EB1FAA8D08733BD0D3940C78382A","60 FPS [Nerdzilla]":"85F98064F17AFE0FB02A99A182AF3FEB7644DCAA","Allow Memory Card saving with Savestates":"2F64F98686E62B60E466E931A9EBCD19A750FF4E"},"GC6J01":{"60 FPS [Nerdzilla]":"A6146772E37970EA5564F51A622A04792158B321","Allow Memory Card saving with Savestates":"D8F327304A88FBC717BB1F775494C5F864B9E8D2"},"GC6P01":{"16:9 Widescreen [Ralf]":"05C9016BF58A628A3FD7B16E1B9BB0769A14846F","60 FPS [Nerdzilla]":"1D3FB017C0D20754E48A8BA54EEE7DC4851BE84B","Allow Memory Card saving with Savestates":"EDEE0E28EEA1834868F2865336290FFBDFB9C6DA"},"GC8JA4":{"Fix C4 texture tiling (used for buttons and some character icons)":"843297019804192AF1FC660BC85F4B10891D0BA3"},"GCBE7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD"},"GCBP7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD","60Hz":"DF2C4DDF082C9E67919443C5ACC485AB2CF7F8D0"},"GCCE01":{"16:9 Widescreen":"BA0066518E7EA8599A81F8496BD39DE20CC369D6","Fix GBA connections":"483BDB94615C690045C3759795AF13CE76552286","Fix buffer overrun bug (crash at Goblin Wall)":"6C107FEC15C76201233CA2645EB5FAB4FF9751CE"},"GCCJGC":{"Fix GBA connections":"4C104D24329172F5D0F8649DE9423B931FE72CA3"},"GCCP01":{"16:9 Widescreen":"2580E7379633CF4DE13B9EC945DB17543477828A","Fix GBA connections":"2EAA60A8A115AD68A795109FB59E4A726D29016D"},"GCN":{"Fix C4 texture tiling (used for buttons and some character icons)":"601FE183C9524ACCF068874DABD73921C86769CF"},"GCVEEB":{"16:9 Widescreen":"ADA686C90189D1D7D23E23E525EFC96EFA104BB9"},"GD7E70":{"Deinterlacing Fix":"6F8B6E33214162E5CDEBD2BC6A60FAFA04D2EB10"},"GD7PB2":{"Deinterlacing Fix":"3B798C9D63E755349B253FAF9186BAE1F703D96A"},"GDREAF":{"Fix audio issues":"F8EAE60FEB0CFB4477FDC4B9E136B63F68DFA63A"},"GDRP69":{"Fix audio issues":"E23D98B2CE185C3993A40F2495D37E41B971BF91"},"GEDE01":{"Fix startup hang":"21068C3CE905FB0CFFAA7408A93154AF8A5295A2"},"GEDJ01":{"Fix startup hang":"7061F3CF11BF64D3BA7F32CCF2BAC42FF3614AB6"},"GEDP01":{"Fix startup hang":"6F1B00517CBA30BEB738EAA90E71221378CD570D"},"GEME7F":{"Force Progressive Scan":"CB04E00918C9C0F161715D21D046ED6620F7ADEF"},"GEMJ28":{"Force Progressive Scan":"41D8F2C833C0B7B79FCA038752052059207AE4D5"},"GEZE8P":{"16:9 Widescreen [gamemasterplc]":"5CEBCFBEA4E444495D2FD6D8B6607DFB2349CC1B"},"GF7E01":{"16:9 Widescreen [gamemasterplc]":"C59CCA3EF8A5E97B32EB64DB9AE80E652ED281C6"},"GF7P01":{"16:9 Widescreen [gamemasterplc]":"1230053B5C347B62E432EFD6635433A183D18619"},"GFQEA4":{"16:9 Widescreen":"5CD9568CE58EF82EB371594667015C9B799454B9"},"GFZJ01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GFZP01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GGTE01":{"16:9 Widescreen [darkludx]":"EF128E7A9C22676834F558BA0F0F7FD8B9028727","Experimental 60fps":"D40344111D989EA009901F8B1C45B5AC8D39E6D2","Simple 60FPS":"5232A937D1D813FF58DD71D716284ED6AB535689"},"GGTJ01":{"Experimental 60fps":"BEC2310911003AF9E4B984A4051E0F885B8CC825","Simple 60FPS":"D62E777A2F019D7BD0AFFBCE876BD9AE408F1667"},"GGTP01":{"16:9 Widescreen":"FA9CD330ECDA01275DA88BD0803DE47757D16A4E","Experimental 60fps":"EB1ACD613BB432A3391CFFF1F0145A6ACE66B210","Simple 60FPS":"43FAD0DDD130BE1E4C8C7603EF6CA7DAA0DF5EE9"},"GGVD78":{"16:9 Widescreen":"9D07DBB5EC2FAA47F2E4587FBD75FD6F3E7E91B0","EFB Copy Fix":"FE52240DF6D132C15A8324E8A477F2BF2250D208"},"GGVE78":{"16:9 Widescreen":"86E561452235BF88D41884558EF34F54CE0FEB48","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVP78":{"16:9 Widescreen":"F9B2108D833084FA36A53E00F1647A9579F847D3","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVX78":{"EFB Copy Fix":"740F2D1C01DA39D1760D96B03974A48E6F74578D"},"GHAE08":{"Fix audio issues":"9799AFF8463EC86C9230E31E2627E141F0C129D3"},"GHAJ08":{"Fix audio issues":"B45A8FC32D14567B8D6C95F303E00A72C0E1D344"},"GHAP08":{"Fix audio issues":"BC7F3CFC97593AA2055C370C175950DC478D2709"},"GHKD7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKE7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"9BFBDE9184BC05EBD545625DEE486E3AAB36B6A3"},"GHKF7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHKP7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKS7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHLE69":{"16:9 Widescreen":"742BFC79D8F0BA5D6215772B58F5A0EADD7BFAFF"},"GHQE7D":{"16:9 Widescreen":"520E5F718D3BD1291C55021776091E0DE0FB4822","Limit internal frame rate during startup (speed hack)":"F6BF450D22104FD2527122EA6E81B1F70E916B95"},"GHQP7D":{"16:9 Widescreen":"AAE14CDC13A0C082A850DED85417100A8C8A0687","60Hz":"884A33613AE8D916128E3FF39B3AA9F63DAADC58","Limit internal frame rate during startup (speed hack)":"BD475ADEF145AC9226514CD1B2D4D5F3BFE67964"},"GHSE69":{"16:9 Widescreen":"3538CDBD13FA939B7BA5F0F82253401AFDD1E2CD"},"GHSP69":{"16:9 Widescreen":"409754E2EBB6F05DEE1AFC647E25B15D462638FF"},"GICD78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICE78":{"EFB Copy Fix":"5BF55685B8867A85EAA9C86571309B17BF7DED32"},"GICF78":{"EFB Copy Fix":"85AABAEB9A59C4F96D9330A3B884F6D757DA1683"},"GICH78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICJG9":{"EFB Copy Fix":"969134EA21A160EBDA91C0870266E7D1707FDC43"},"GICP78":{"EFB Copy Fix":"13B158CF41F5412BC637F50644193D43CC3DA49A"},"GIQE78":{"EFB Copy Fix":"E15AA1E30D26E5735D68AAADE436E7B7E4A33A35"},"GIQJ8P":{"EFB Copy Fix":"FFFCB76E98DDB06A7BBBC0AA73C869C87EB787D6"},"GIQX78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GIQY78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GK2":{"16:9 Widescreen [Vague Rant]":"6B4C59C2A2C5D71C7A38C8513A76E6467E307A26"},"GK2E52":{"60 FPS":"8F2397930A50C3C176188526EB9612578F9CBCE5"},"GK4":{"16:9 Widescreen":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKB":{"16:9 Widescreen Region-Free":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKBPAF":{"60Hz":"8E4E0ABA0E6D102A33206F34F4DEE63159B3CB4F"},"GKDP01":{"16:9 Widescreen":"DF4657937DE730107B636C73E959277EB963D210","60Hz":"6740D7B914CC897AF50798C0AB391965294FD1E1"},"GKRPB2":{"16:9 Widescreen":"69C01A4F91D991CF866237FED94A138308FC104F","60Hz":"40D06A5C7A3D873ABC73DEDF573738C2E3E37FF0"},"GKWJ18":{"16:9 Widescreen":"696570101FA040778EAD310377C484C846D87430"},"GKYE01":{"16:9 Widescreen":"6520EF1B7D88F00747B120A3A010458602ED989D"},"GKYJ01":{"16:9 Widescreen":"47B8EF7D02831AA5C375C698797BF2D5475FEFF2"},"GKYP01":{"16:9 Widescreen":"69413C75036D2975716066E6574461B981FF0124"},"GLEE08":{"Fix audio issues":"7355F358CAC6F418D37E4C23E64F7867D46E4FC9"},"GLEJ08":{"Fix audio issues":"12B24A6D7389A2AC5AB75FC0BF8493E7661F2A73"},"GLEP08":{"Fix audio issues":"81BD39F5527552DE89E3B59BA86298900F0A3168"},"GLSD64":{"Fix freeze in opening cutscene":"5E2A73717BD66EF647846DD64C33BC80AD9B5227"},"GLSE64":{"Fix freeze in opening cutscene":"1CE78E7954415A44DF693C0BB879AA5A4FF059A3"},"GLSF64":{"Fix freeze in opening cutscene":"009B0C4AD80A9C28C987934D254C2C4AACC9A07A"},"GLSP64":{"16:9 Widescreen":"8E7A544C10E7A5E2F0304A0D2586879627EF6586","60Hz":"B67144E87B54246137142992A3BC83DC13BE68A7","Fix freeze in opening cutscene":"3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC"},"GM4E01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"78BE50F93E81A1972CA31ABC318949E366E82BD0"},"GM4P01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"6D950B6EEFD6D304E1A424355B74A595D32BDF2F"},"GMBE8P":{"16:9 Widescreen":"D12DE9E3941BCE6EB50B8DA92140E09AFC0104C8","NA Stretched HUD":"0151A6683E1FBFD20096807C54A6952C4A4BAD40"},"GMBP8P":{"16:9 Widescreen":"8A688981F33A2C53882EF08FCF20B88AD43EF417"},"GMNE78":{"Limit internal FPS to VPS (improves performance)":"DE38BC8493EA24F93B6D805F28746AA99C53C677"},"GMNP78":{"Limit internal FPS to VPS (improves performance)":"65FB9B430F7D58EBC76DAADE8CA9DBBAA32B8B51"},"GMOP70":{"16:9 Widescreen":"CE4D298616BD42E4D8F8936B325CD1FD7F5B956B","60Hz":"0CD2CC787A5FF551901E41C85C8AAC02017ECB89"},"GMPE01":{"16:9 Widescreen":"4F98592DB3DEE3857469A8D8605FAF4BD6F7C76D","QOL - Allow Digital Presses for Map Screen":"E232B27564E9AA0C32DE163C9C056317A7B2B12E","QOL - Automatically Advance Text Boxes":"59607671BFC4717ACAF9807BB7EB0D9F982866D4","QOL - Disable Advance on Results":"CAEA37F3FEF89400513353EB85875F2A3AF4C03E","QOL - Faster Boot Time":"F75FBBD838C5B84FF687962FBA9195B217DE132C","QOL - Increased Board Speed":"19F264DE7F07EAC9433CA4B591BEBA1EC976C9F1","QOL - Increased Taunt Capabilities":"C70391D4961A0E820DE40141C89341369A9F021B","QOL - Instant Text Display":"F4E6913CF034E5778B9F9CA5FF448FF1B78B3333","QOL - Rumble Always Off":"93621947019532F02C25937FD3EEEE21A788CB07","QOL - Show Controller Port Number of Who Paused":"D7249AC3C949FEB860CF4350C5B7B79AC16569F1"},"GMPJ01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMPP01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMSE01":{"60FPS":"2805B1A0DD53EB64877D375D10F667700955720F","Widescreen":"BD718F961DBA5372B1D0257D454D535746C453A0"},"GMSJ01":{"16:9 Widescreen":"E8112A01040A06460E368F142C5D1FD0B4085D94"},"GMSJ01r0":{"60FPS":"9894B5B6B5215844D48411021FF8EECFE851D79D"},"GMSJ01r1":{"60FPS":"2DB69DB7A3753D543CD85CA4C77B4EF0AED7486E"},"GMSP01":{"60FPS":"D07009B710B3BBC6B12E54ED6A7969D58197EDAF","Widescreen":"0B7B89BF0868393076EF5F9DBD689DD0EDBCB84C"},"GNHE5d":{"Nop Hack":"89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF"},"GOCE5D":{"16:9 Widescreen":"E4D800B90F16A15D9252EED1B2A23AEAF5CD230A"},"GOME01":{"16:9 Widescreen":"C33CBBF6F22195BF626104B4827200B3EE1CF30C"},"GOMP01":{"16:9 Widescreen":"874784CCD4AC3DC9C69654E471C18AD0F5E4D9AE"},"GONE69":{"Widescreen Hack Culling":"6412A67E1C26E146A9526AFC9F0EE517B214069D"},"GP4J18":{"16:9 Widescreen":"BF4D2D6AF66F285E81173B3B20D5369DB6FB351B"},"GP5E01":{"16:9 Widescreen":"5ED053787332DB5EEB8B963BA5DF197B58C6BB75","QOL - Automatically Advance Text Boxes":"41EF84663008668682BB75DC8B874E38669AE041","QOL - Disable Advance on Results":"6C36BEFE751131298BC8B39012892F6A6903CE60","QOL - Faster Boot Time":"8E952E6E12639AC20F6F9FDD150C12F0C294DF43","QOL - Increased Board Speed":"4BD72B8ED75ED8EFB3329C104BAD114FDDB8AD2C","QOL - Increased Capsule Throwing Speed":"C5BA3C9D386D09DD6CB9242A12A0D041BEB93160","QOL - Increased Taunt Capabilities":"C360EBDDCB0237076052C983BD9719411CA67CDE","QOL - Instant Text Display":"2CD4A06A3B9C18CC9EA2A2DEBD96E066CD1D700A","QOL - Show Controller Port Number of Who Paused":"5BA3648D18346EDC97B23475D4748C637E6095A2"},"GP5J01":{"16:9 Widescreen":"81E69B6BEE85E06805EC24E1CFEA9405BF459DF8"},"GP5P01":{"16:9 Widescreen":"FD4FCCB97C06F0173A30D4CC9A99422B2C0ABA9B"},"GP6E01":{"16:9 Widescreen":"B502AF0887792E9B140516D14BA75F99772C17BE","QOL - Auto Advance Text Boxes":"E645DF70CE0266D174D917A817A7FBEEE974A9F0","QOL - Disable Advance on Results":"798E566B621F6A32BF23C588E76EAC0776E2A8A3","QOL - Faster Boot Time":"B089A3C80D5DA86ABF6258F8BFB1DC78BB54A973","QOL - Increased Board Speed":"501C52ACF117950888A66DB2BEDBDFD7D9B20235","QOL - Increased Orb Throwing Speed":"845F439278DA8CBC225658FF4E2F82D707422BFB","QOL - Increased Taunt Capabilities":"0AED27341A06186AD92D5725C0B9693CA8F85B7B","QOL - Instant Text Display":"5AAC4E21927D4AEE7AC329CE793D4CDAEDDF7A3F","QOL - Show Controller Port Number of Who Paused":"DF5D2F068D76ABF2B1C294D413B9154B8C356929"},"GP6J01":{"16:9 Widescreen":"88F50F8298F82E3E5C161277BD4A985CE893A9D0"},"GP6P01":{"16:9 Widescreen":"DCCE59CA987624C753F0A86BEBA4287635901C93","Remove Black Bars":"138A86D43B5830BFE1926F58C0A4FBE2971BB02D"},"GP7E01":{"16:9 Widescreen":"87EBF72D90503EBC5CE9BC994795669E48A92A4A","QOL - Auto Advance Text Boxes":"82338AB5D82A7F398B4EB674CA4866DD1F4A1E65","QOL - Controller Options Always Acesssible":"D2F9CC0B9E52136E77187395FB7DA9B64B2C3B30","QOL - Disable Advance on Results":"A644551162A61DE7997B2A84F5BFDEFFD3FC2FB5","QOL - Faster Boot Time":"1939B5C575AE8FF6890BDDDD1B3FEA472D8CABA0","QOL - Increased Board Speed":"7E06A2EBCFECADB8074152B489984237A6FE9F21","QOL - Increased Orb Throwing Speed":"6671BEE71920D6B845AE1257AFE3AB3B95CD6FBD","QOL - Increased Taunt Capabilities":"9F60923F95FFE709CCD06966CD2DF743263934D1","QOL - Instant Text Display":"48FA6002E42DA9BB83ABC980A32C60CE3B21C4FC","QOL - Show Controller Port Number of Who Paused":"5E6F8C6C87D5944DB18A4E4F9A45F61EE87A4163"},"GP7J01":{"16:9 Widescreen":"A081F0729068D233E99DC00C64E36CA33C945640"},"GP7P01":{"16:9 Widescreen":"75D6CDA4EA301A71F7701A0487544E79136850B9","60Hz":"6D0D5B7CA58C38A3147F8ED981B5B21CC6C519CD"},"GPIE01r0":{"16:9 Widescreen":"2B58166A66644F984E64077A6245C94C79B51063"},"GPIE01r1":{"16:9 Widescreen":"9A1545AB4E2B9216B95AFFA83420D723512DA8C8","60FPS":"3337C767EAA21D30C6CFEAE9985FFCABFC63E2E3","Disable Pikmin optimizations":"CBD15E9104929F0631713AE482475CB2B90E71F2","No Blur":"A25CDDB79991F090BD7F12840AD06F13BD5CCF95"},"GPIP01":{"16:9 Widescreen":"5AE44D5B0E0D4C412B453B95CF5A41DBA4D685E6","60Hz":"AE59FF751E167632AE283F38CC4B96BDF7B9B81F","Turn off blur":"4ED79A548DBD7DBD35928A2F1138315FF103E260"},"GPNE08":{"16:9 Widescreen [Ralf, darkludx]":"ED3D9BB08C5F1D21BC8D5C73155C00E3C25C466F"},"GPNP08":{"16:9 Widescreen [Ralf]":"469B18FE8219031D355557B836EF02B625040A4F"},"GPOE8P":{"Make Save Copyable NTSC Port - 1.0\/1.1":"7A4B3E596BA4FDA8F3FCF984079B3F44CB2D6AB3","Make Save Copyable NTSC Port - PLUS\/1.2":"304DB311F2107E88EBCE058535B266F9263AA8E2","Save Validation Code v 1.0\/v1.1":"9F6259132453DCC0607EAB35DB182C76818F2F9F","Save Validation Code v1.2\/Plus":"17EB6CFB408EF27D44C053A1336C3B87B6A05018"},"GPOE8Pr0":{"16:9 Widescreen":"99986BB1D4ABE45C429D48B01BFB578FEA69C14F"},"GPOE8Pr1":{"16:9 Widescreen (Plus) [Ralf]":"5563671A6702785FFE106DAB7EF3F4FE6D9F705B"},"GPOP8P":{"16:9 Widescreen":"DF133C61C29D5CFA87A2A785C962C205F80E6D04","Game Save Valid":"40EC1590DB26C7C58D00E3065F1E404341EF0E73","Game Save copyable":"2F5FB98927DAFF141D1CCD1EACA8AF626397E284"},"GPVE01":{"16:9 Widescreen":"E5B51080CFD518FE584CF3B61A1099CD032479D6","60 FPS":"6662E99E41F77763EEE524477DF341836EC7032E"},"GPVP01":{"16:9 Widescreen":"78F3C9A789827B063A6701987262276A66ABD82B"},"GQPE78":{"EFB Copy Fix":"880B114E9A308084CAB92C004A9EE067B371C310"},"GQPP78":{"EFB Copy Fix":"5D9A14954AE8D639C9B254F3BA73A70F284BBC8D"},"GQSDAF":{"16:9 Widescreen":"CA82C44B2E8FC5C184E3FF935BC89661B7DB55D6","60Hz":"952165FD78543EB6D2F5230F7570B0513773D332","No Blur":"DEB7DF358423F0EF30C9FD49F4F14590836D52A6"},"GQSEAF":{"16:9 Widescreen":"48AFE1D32843C6B5AD4337A49F15FD447DF2A752","Remove Blur":"EF448CF1FA6A15EB2661EA5338703C13D52EF65B"},"GQSFAF":{"16:9 Widescreen":"76C30E1737CC5C3B5DE32532B5F9A5CD50B690F8","No Blur":"986BAE5103CDE9286D2D34FD28FE2540D0759AAB"},"GR2E52":{"16:9 Widescreen":"5F55CE7DA7E1E3261CF2FF88F34CDA63BA511003"},"GR2J52":{"16:9 Widescreen":"D3C6FA690BED07BFA65C2E1FCDE2FFA5AA0EEEE4"},"GR2P52":{"16:9 Widescreen":"D7BCADD4E76B5E3F7D9D463F55A2777528E37E94"},"GRNE52":{"16:9 Widescreen":"483D74C47AD1012606D591A226AB3144C7FB201C"},"GRNJ52":{"16:9 Widescreen":"949281B7221B20680B7BC29E4754B73A326EBE85"},"GRNP52":{"16:9 Widescreen":"11051B094FE5A8B2E73060BFF786C1588E36979A"},"GROE5Z":{"16:9 Widescreen":"FADBAB5EC280CF8ED4C9536A33F4184BA210C9B0"},"GRYE41":{"Disable Culling to Fix Rise and Shrine Hang":"AF0A575EB6071EAC0D2EC3D2EA30A23EB05A4192"},"GSAE01r0":{"viWidth 704 Aspect Ratio Fix":"0D529180B9B28BB06E21EDBDBA61C17EEF0FFB7F"},"GSAE01r1":{"viWidth 704 Aspect Ratio Fix":"CB15ED22625690A5DBE4607FE30160125421461D"},"GSAP01":{"viWidth 704 Aspect Ratio Fix":"749EDA62B1B73354599929F1B50E992E111C5493"},"GSNE8P":{"16:9 Widescreen":"CFDF3FA5B48B347E49E8E521D7BE47FCC9A5CBF4","60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSNJ8P":{"60Hz Aspect Ratio Fix":"98DF9D67DE77F66A4B09F25C5621805DEB40865B"},"GSNP8P":{"60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSO":{"16:9 Widescreen Region Free":"0D60D0593F6DA28B1236381B22082506B9533F0E"},"GSPE69":{"16:9 Widescreen":"80E18A8963F4EFD3C03494C95934452FCB6E521B"},"GSPP69":{"16:9 Widescreen":"162E87E9F5511CD82216811055836C11B374C45B","60Hz":"E5DF2DF34D2F0BDEE4205C891576511EC572C5DC"},"GSWE64":{"16:9 Widescreen":"6480326CCA8FF64A4868EC6E9FDE9F38819A75A1","Disable Dithering":"85F65E4B627641862C1A6091D29A3BB0626B04C3"},"GTEE01":{"16:9 Widescreen":"6B57EBCB9CC0ACE2EDA0F13E3F1F55F3FABC3A26"},"GTEP01":{"16:9 Widescreen":"72785784363C46180AF8E388348FD77667E7D382"},"GTOJAF":{"Remove Blur":"53A2EDF113F2ED7E54A16AA0E73FDB2C44C79DE5"},"GTRE78r0":{"Limit internal FPS to VPS (improves performance)":"42B85EFC5DEF8B243521EAD434DDCBDF95664CB1"},"GTRE78r1":{"Limit internal FPS to VPS (improves performance)":"ADB3A41B992EAB15AE484B2E575ACF3354415186"},"GTRJ8N":{"Limit internal FPS to VPS (improves performance)":"47DBEFDCF22ADC2AB148A54AC885FE19B3C07E3F"},"GTRP78":{"Limit internal FPS to VPS (improves performance)":"E8752E1B60DF76CD0CC7408074E742B0966BC904"},"GTZE41":{"16:9 Widescreen":"ECCE87E0006475C73E7D936AA0150BD20166F9EF"},"GTZP41":{"16:9 Widescreen":"62D8F159B93167171E0860D96C4BB32A5BF5648B"},"GU2D78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU2F78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU3D78":{"EFB Copy Fix":"8A0E3114862ADFE421874211BD6F5220AA425BF5"},"GU3X78":{"EFB Copy Fix":"E3303FDAE7ECA17A72EDC440C32D94648A6453A0"},"GU4Y78":{"EFB Copy Fix":"D54767785E139A8BC8C4B75573FBD5A0B686D8E3"},"GUNE5Dr0":{"16:9 Widescreen":"C942EBCBE8A487C25E296EC1FAC2358DA1487DBD","60 FPS":"A441630EC5FF0EB74D2243A3092D22E69C6BEA02"},"GUNE5Dr1":{"16:9 Widescreen":"39BD84CBE2AFACFEDEC5E9020D1A8B0D36DD53F9","60 FPS":"05779F1A3D0C43305D52A95447D612CB424942C0"},"GUNP5D":{"16:9 Widescreen":"FE3BA1DAA1AF278A7839E27BF466B0BA0B390EC2","50 FPS":"3B20499A3F8D1D7CCA5B0015F2D80ECED25842A6"},"GV4E69":{"Fix 2D Rendering":"8679891FCAA250FCFF670B26E0CB9875900D17FD"},"GVJE08":{"16:9 Widescreen":"09EFDA75D876A675121C8344D8FDD09A70A1A846"},"GVJP08":{"16:9 Widescreen":"93938F4C6112C91549F2B52E6259170F7501EC07"},"GVPE69":{"Fix 2D Rendering":"3159CA79B0A890131763EA6CB163684BEE886E3F"},"GVSE8P":{"16:9 Widescreen":"4CE9C491160A4B631142EE9CE802C694163F1CA3"},"GWRE01":{"16:9 Widescreen":"AC42770B06662BE1DC863EC80F44B5E034C63664"},"GWRP01":{"16:9 Widescreen":"9DDDFAB28C4BD35CF64050E1EF684DC042B1AFFA"},"GWWE01":{"16:9 Widescreen":"98B2E75D8E1CED4A964D3129A5DC10E30538CAA6"},"GWWJ01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWWP01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWZE01":{"16:9 Widescreen":"9EFC191DE6D21A1681FE241AB2EE4A131259F317"},"GXCE01":{"16:9 Widescreen":"901A1E78A3A0124F55548507D3B3707125C64A8A"},"GXSE8P":{"16:9 Widescreen":"F50BBA440184FC77A4DFFAA58FF2BB888E2E072F","Aspect Ratio Fix":"4214C74DFB8A74FC3AA4A643E869BC4D9A38EDA0"},"GXSJ8P":{"Aspect Ratio Fix":"227909607984BBC3D36AAC7DB9DFE385F3363C49"},"GXXE01":{"16:9 Widescreen":"7CAFDAD7B5E7459CCC1BB209D439DDCBB5BE4E4E","60 FPS":"7E2829049003FC9DC8BC28044B33CAFF3768B54C","Allow Memory Card saving with Savestates":"64FAA15062F0D0C319F904BBDE9C4489A25D6369"},"GXXJ01":{"16:9 Widescreen":"AF704A8C6838FDF47054D9EDD9106B3F46781FA9","60 FPS":"4F5FD45B4EC1B33FE076FA06F8FF863C46A06BBA","Allow Memory Card saving with Savestates":"8293802260536FA2EF2EFDAB5266DE36BB88DE1B"},"GXXP01":{"16:9 Widescreen":"02716B2585BEE74F8FCDD97A78F6A0D3DC7F331B","60 FPS":"BF6171EA5CCF4B57AA596372589DDB5E6904DD7A","Allow Memory Card saving with Savestates":"3CAFBC4AE6FC5CE9F53377F86AB5BD8F1BC8861A"},"GYFPA4":{"60Hz":"402ED10AC842041AB4B39AE8F2D81B2D7AEF9CB4"},"GZ2E01":{"16:9 Widescreen v2":"E7521ED27BFC972628906CBE8D5403ED57253BB8","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2J01":{"16:9 Widescreen":"F985A0A58D8E2B23E8A557FAFF8D367AFFEADD07","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2P01":{"16:9 Widescreen v2":"CAC38B0D334B925A57AA3118D35932B8A185137E","Hyrule Field Speed Hack":"0F63623D4D984B7706F718F57C0ABDB6DBADCF8D"},"GZ3E70":{"16:9 Widescreen":"167F9E9A9B372CB3A01F308B46FF1403F8599C51","Deinterlacing Fix":"EF1291C0E45277CDAE4D0F61E1949C1E75E5A3E1"},"GZBJB2":{"Deinterlacing Fix":"19D67BD12A18F0E3A960BBB92E2F490BEF5C00C1"},"GZDP70":{"16:9 Widescreen":"2E424E0BBAE6EF5D6A8FB4224EA1D0746BAC37D3"},"GZLE01":{"16:9 Widescreen":"9FA864EE7DD8CE7FF538EB4E0243F20137430BD8","Remove Distance Blur":"78EA34CEF8E01701491C280F155F1C12EC9BF1A2"},"GZLJ01":{"16:9 Widescreen":"E03B61989025CA33937E63A057E6E40A403811BC"},"GZLP01":{"16:9 Widescreen":"3EFFF6C52B5633A1729FAA6883D8579E77F7D057","Remove Distance Blur":"14BECBEA4DD281EBD0F7FE7DEE8020B6F2418ACD"},"GZPE70":{"16:9 Widescreen":"7142F2495507AC7136992128ED0FCA6BC72B61F4"},"GZPP70":{"16:9 Widescreen":"591FD6C9668C79FF1CE8558EBED2486A7A327F05","60Hz":"A59B84DB5486521AEE1C23B6C741ECD35216E5AD"},"GZSE70":{"16:9 Widescreen":"8E86EAC7EA4F4D2854DD9020CD795630CA64C4EE"},"HAF":{"BufferPatch":"181195871F63B89B1CF09AFA4420CF89B9883108"},"HAL":{"RSAPatch":"AD12237401ABE9FE4A545AADB5C5AE10355E2076"},"P1RE01":{"60 FPS":"FD15A2EC38E46C1C3153336BC44F11B08ED906D0"},"R7XE69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"R7XJ13":{"Speed hack":"4590D425BCBAFA0D1E32D27B46068843F6A2BE09"},"R7XP69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"RB7E54":{"Limit internal frame rate in loading screens (fix hangs)":"BD1A252B43FD339391ED33554542E786BAD6671B"},"RB7P54":{"Limit internal frame rate in loading screens (fix hangs)":"90247F89DF8EB7A4AFA2C425CD3006257A95A8F8"},"RGQE70":{"crashfix":"5F4CF8D4DA19A0FF74FF9EB925AC0236069BFD59"},"RLEEFS":{"Fix crash on main menu":"793642AC6862C2F3412035A9E3D7172CC4A1D5C7"},"RM8E01":{"16:9 Widescreen":"E61344EB1542A78D497981C307B6549985C7A05A","Extra - Disable Music":"25223F9EFAABF601CAC7810004F124E4056598B1","QOL - Faster Boot Time":"BCC4279F8B28636AD773F01540E78DF40EAD6087","QOL - Increased Board Speed":"B65AF1819966CD3435D88801E8C79704E3A52DB5","QOL - Increased Taunt Capabilities":"66495D7CB532FAE778AFC22CF45D17D0FFDE5310","QOL - Increased Text Display":"B4186DDC54F33F4D6A22188EF50CEB43FB205673","QOL - Invert IR Stick for GameCube Mod":"9D90A9C66AE8AD91B201B40C4145D1323B701A77","QOL - Remove Explanations":"330DD53AB993A99576564FEFD222D7BD211B878F"},"RM8J01":{"16:9 Widescreen":"A140BDCB8E1721CB6B4CD878E412113322258B57"},"RM8P01":{"16:9 Widescreen":"F664C32AFD3D785FC6E04D8990A3FA1C72A18C5C"},"RMHE08":{"Bloom OFF":"CCF233DA57B3E75221870DE502955114B0D4E7FA"},"RMHJ08":{"Bloom OFF":"29D3625B7ED577587E56AA07CB0EB8C47C97E823"},"RMHP08":{"Bloom OFF":"1720C1173D4698167080DBFC4232F21757C4DA08"},"RO2P7N":{"Hangfix":"EEE9C8DE4671C18DD7F81DD08D39B64C57600DEA"},"RPBE01":{"Fix black screen effects":"775ABECA6073E02C5C68CF4D644194D966A418F5"},"RPBJ01r0":{"Fix black screen effects":"0EAB5D8DE827894AFEF97C10ACB67378E6983323"},"RPBJ01r1":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBJ01r2":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBP01":{"Fix black screen effects":"82AEB60F9A9083F93060531A970FFAABE0833A40"},"RRBE41r0":{"Idle loop speed hack":"20C7A2D4A3D6D55A8029A9D200D57CBD11F60B88"},"RRBE41r1":{"Idle loop speed hack":"0A30FA4C7E787197A00B7267368F372EFF623776"},"RRBE41r2":{"Idle loop speed hack":"2227787007F16F0CD454422AE6EB94656464B6D0"},"RRBJ41":{"Idle loop speed hack":"E9209EB728AA13375B2ABEF1C15D066DAFBF2EBF"},"RRBP41":{"Idle loop speed hack":"8A47BC311138EE3A11E049DF3EE6AED38990FB49"},"RTH":{"Disable blur":"812EE46AC967BFCD239335B10A664D71A93E8175"},"RUUE01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"B62652D5BBE0701900090CFEB32003AE6E5A5015","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"244D1FC7D6284DC8832CF7F34506B0DE4DBF8914","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"6FDD09D51EC3D754BA7F80630B1FE1D3FEF6FE97","QOL - Disable Grass Deterioration":"D566F2AF5BB1EDB8284461DEA85FF3B5A8DEBDD6","QOL - Enable Fast Text":"C7D8A7926ED952A2797DA312B41B08DCCDC3A274"},"RUUE01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"FC7BF1D6A5B4ED39EA4671C241E78F3BC8037E97","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"0235631B6F9A439B9718635F5AB7ADAFA2B6B3DE","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"5DEAC5F3390CE19487A9473A1988E7BB5EA4790F","QOL - Disable Grass Deterioration":"0FE1EF4DE5C8AE691E96634BC66BA034850C6C0F","QOL - Enable Fast Text":"B2BFF779434FDBEF08FFD73C4B140FCE0A8FDF0E"},"RUUJ01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"D79F773E1363B694784C94DD309709D41D368A08","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"C88CFD7E6C18F570DE974D6C0202987E4F1B72C4","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"3C92010E7DE2E93C48AFF25B30A18BBFCBD12707","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUK01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"1B89E8A6E6E09757373FF116BBC0989C06651709","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"EF883FB0C237BF51ABD88C35D34571D240C79484","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"F08D0F233F3F5833DFAFE5C8049C37D32360F0FA","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"C55A5CDEF4C868E66C1FB2902D3285B2C350D7F1","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"D28D4E4FBD153A6D4C49E2D250213F37BEC941E5","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"80C3F50C018E076DF2CD39732A5ABB8A1C0E88B1","QOL - Disable Grass Deterioration":"3B278C19AC76D27488C55201BA0DB63864F66AAF","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"00D994940AD323786A9DA780D1347AE5FBABAC8A","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"9121BEDD72436F4EAB47E4CE96ABD864118B22F3","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"8FC58B3DC4817515822EBB82BB7A8055270EE51D","QOL - Disable Grass Deterioration":"9B199D77AF748518358D32966F3F55F5C5759C3A","QOL - Enable Fast Text":"827CF5DFC90A258C36BF32DC3883371973E0B706"},"RX4E4Z":{"Fix file reads (dcache bypass)":"9E4E0F1465A9A1E85349DBA3B1278AC215A97DBB"},"RX4PMT":{"Fix file reads (dcache bypass)":"EE85907C03F0295794821383B93F8D5B91D2697A"},"RZDE01r0":{"Hyrule Field Speed Hack":"15EAD073414C9903D6CAE5229DCE582BD17A9162"},"RZDE01r2":{"Hyrule Field Speed Hack":"27395CC8BC2C51201D566657D31A471A850482FB"},"RZDJ01":{"Hyrule Field Speed Hack":"B3F7473F8C911A32F1D616491C9E78EBBD7A6309"},"RZDK01":{"Hyrule Field Speed Hack":"A280C0114B800D7DC056ECFB5E482229DA0B1550"},"RZDP01":{"Hyrule Field Speed Hack":"2A83ADFB760F9498841ED0ED68B0C0438232472C"},"SAOE78":{"Fix crash on boot":"EA11FA4908FB20B61876ACD360EC7657A6D39FB2"},"SAOEVZ":{"Fix crash on boot":"AA55C214DE7545DE0E203CC39F06BF3D31451BE9"},"SC2":{"Limit internal frame rate (speed hack)":"B3FB0FCE108E85E6198BAF3535850470049E2BE2"},"SGLEA4":{"Fix black screen":"258378187ACF475A55EFEAF8A703681252E014C3"},"SGLPA4":{"Fix black screen":"6F8CD59D897338CA90939149E1A62588620C6D88"}} \ No newline at end of file +{"D43J01":{"loophack":"CAB9CED2D904F12CCB21F5B1DE9B5433620C3E13"},"DPOJ8P":{"Bypass Modem Detection":"90BA66E25640A538FEFA6693715718139079FA7B"},"G2BE5G":{"Disable interlaced rendering":"7FFF6BDD93713BEDFD23739C32B86153FA19AEA0"},"G2BP7D":{"Disable interlaced rendering":"56E85D7285F10348E1E5354E379918D07E79EDA9"},"G2VE08":{"16:9 Widescreen":"3ED033396382424533A263A39687A0961EC994A6"},"G2VP08":{"16:9 Widescreen":"EEBC3C5F04BB70AFCA1A4C3D638335CDC9142BA2"},"G3RP52":{"16:9 Widescreen":"7F6B7743F0DDD3704B15882330E490103AE24E36","60fps":"4C9A8572A35F1DB85BBB2F32ED34288A343F1FC0"},"G3YP52":{"16:9 Widescreen":"3D102AFEE2F4945894E74BB702ED3ED2294C6FEC","Force PAL60":"FD61581C593EA34DD358AE33819FAB310F2CE961"},"G4AEE9":{"16:9 Aspect Ratio Fix":"8C2564CF47BB6A9B57D712C90E17F459B0BB6CCD"},"G4ME69":{"16:9 Widescreen":"6FC6E5D91693FAABE25CC721F2121CB0B5CA9D12"},"G5SE7D":{"16:9 Widescreen":"16119398BFE241A84151C046E0BC5BCF89D22049","Remove Bloom":"A5F2FB2B3BFF61F34F8AB1DD5A912A0FD8DFA720"},"G5SP7D":{"16:9 Widescreen":"718AA8BD831961C33114852FDA75BB58F768F46A"},"G8ME01":{"16:9 Aspect Ratio Fix - Centered HUD":"8C2FC3FC6F7766AA8E87425BF3BDD2E46E73DE71","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"AE3DBE2604CA0660165CA870C4B80E2B523C9475","16:9 Aspect Ratio Fix - Normal HUD":"F6E8264CF14D0189219C1E2D20C4182F190F4B88","16:9 Aspect Ratio Fix - Stretched HUD":"8EA4C7B80ADE3E8FEC28EEFA3A046B6B04162E81","No Letterbox":"B1DE2A3526F2E4511F2B94A5171BF7B3C2C52B9B"},"G8MJ01":{"16:9 Aspect Ratio Fix - Centered HUD":"B3181CA1AAB11EAE9132184DA48D76E775AC0A24","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"904C0EEEF16D761F7BDF7099DEB384DD9AB26E00","16:9 Aspect Ratio Fix - Normal HUD":"631CE99BDE3F1475303C00C67E3C130D4EB2547B","16:9 Aspect Ratio Fix - Stretched HUD":"81F7AD591F12A01DE82850457ECF147998D1238F"},"G8MP01":{"16:9 Aspect Ratio Fix - Centered HUD":"75AF5D7FA49BE9C3C73EA1482B49CD9B36CE817E","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"C2E5003E74551DF693A7CB814850CA51D77549EF","16:9 Aspect Ratio Fix - Normal HUD":"FB26AA84B0CB787705E84E869B649307776A2839","16:9 Aspect Ratio Fix - Stretched HUD":"C5F8A81520193418C07C7BC4881F02BE9C4DC9BC","No Letterbox":"7828F5B0C8B470620755C16C60A5A4554F92640C"},"G9SE8P":{"16:9 Widescreen":"CD043229AC47AC93D537B6A0725A2D0BB6FEF4E8"},"G9SJ8P":{"16:9 Widescreen":"C55EF90B542AA3973352A232629A828BC22A8509"},"G9SP8P":{"16:9 Widescreen":"9521774ADEE769CFCCE86F8170FE626387E73019"},"GAFE01":{"16:9 Widescreen":"D24F6D9BE5D79D3C795C2844634A0214BB68B5DA","Make Game Save Copyable (donny2112)":"F9A812FF62A20D5440CAB79DB6AFAE068457A10E"},"GAHEGG":{"Limit internal frame rate (speed hack)":"2873E5178C8068AFA8AFE751F903A69CA9A41F16"},"GAL":{"Trophy Viewer":"DBDB50216B22CDFFF77BDD80261CD0FF75803D3B"},"GALE01r0":{"Normal C Stick Functionality in Singleplayer Modes":"9DC8D90D986FAC175F45006AFF20CF00EDFBE053","Proper 16:9 Widescreen Support":"18B4159E77013EDFE499A33281B771C442970E3B","Properly Display in 4:3":"DC2BCA7D99EBDE1DC7C68CA6AC7F543B93FD6A67"},"GALE01r1":{"Normal C Stick Functionality in Singleplayer Modes":"F6030959C1CAC92F37287D556F18B3C5298FAD5B"},"GALE01r2":{"Disable Rumble":"CFDEEA8AD2A99394BFFEA7211EF03394E7471FA9","Normal C Stick Functionality in Singleplayer Modes":"BCD918A72368B2A30E797C76994E7539A1C0B5D9","PAL Stock Icons":"6B92D95F0EEA165EA84FD753F74B0A9E2079C2D7","Proper 16:9 Widescreen Support":"CEDF18354CDCFC49398551C629E51B053331A55B","Widescreen Support v1.2 [Dan Salvato]":"D32ED4CD685F3FF649F7E749E8C182C657013918"},"GALP01":{"Normal C Stick Functionality in Singleplayer Modes":"4817F202678559291F9F76B46DC7561A45D00B77","Proper 16:9 Widescreen Support":"BFCBE098FE8679B9140A48EA1D8620D5A369697C","Properly Display in 4:3":"5BA2933B8365E21553AA273B91954A4C722B1035"},"GAZE69":{"60FPS":"BC281053C5266B21FDDC6C8996A6021DEA03D459"},"GBLE52":{"16:9 Widescreen":"C0F3AB3BBC9B21F4BF25F44E48CF1CD36D2B3F45"},"GBLP52":{"16:9 Widescreen":"797040CB097BFD369490A1BE29659929D76CE6F7"},"GBRJ18":{"16:9 Widescreen":"B3C867AB34D90E2E9C5B799D800F4C191DE279FA"},"GBSE8P":{"16:9 Widescreen":"4148D453F61A5B81E53669335BC667E651865C76"},"GBSP8P":{"16:9 Widescreen":"6CB029AE768BA5B9995D5F12C1EBB37B3D1FE242"},"GC6E01":{"16:9 Widescreen":"DE932CCAD1B5EB1FAA8D08733BD0D3940C78382A","60 FPS [Nerdzilla]":"85F98064F17AFE0FB02A99A182AF3FEB7644DCAA","Allow Memory Card saving with Savestates":"2F64F98686E62B60E466E931A9EBCD19A750FF4E"},"GC6J01":{"60 FPS [Nerdzilla]":"A6146772E37970EA5564F51A622A04792158B321","Allow Memory Card saving with Savestates":"D8F327304A88FBC717BB1F775494C5F864B9E8D2"},"GC6P01":{"16:9 Widescreen [Ralf]":"05C9016BF58A628A3FD7B16E1B9BB0769A14846F","60 FPS [Nerdzilla]":"1D3FB017C0D20754E48A8BA54EEE7DC4851BE84B","Allow Memory Card saving with Savestates":"EDEE0E28EEA1834868F2865336290FFBDFB9C6DA"},"GC8JA4":{"Fix C4 texture tiling (used for buttons and some character icons)":"843297019804192AF1FC660BC85F4B10891D0BA3"},"GCBE7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD"},"GCBP7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD","60Hz":"DF2C4DDF082C9E67919443C5ACC485AB2CF7F8D0"},"GCCE01":{"16:9 Widescreen":"BA0066518E7EA8599A81F8496BD39DE20CC369D6","Fix GBA connections":"483BDB94615C690045C3759795AF13CE76552286","Fix buffer overrun bug (crash at Goblin Wall)":"6C107FEC15C76201233CA2645EB5FAB4FF9751CE"},"GCCJGC":{"Fix GBA connections":"4C104D24329172F5D0F8649DE9423B931FE72CA3"},"GCCP01":{"16:9 Widescreen":"2580E7379633CF4DE13B9EC945DB17543477828A","Fix GBA connections":"2EAA60A8A115AD68A795109FB59E4A726D29016D"},"GCN":{"Fix C4 texture tiling (used for buttons and some character icons)":"601FE183C9524ACCF068874DABD73921C86769CF"},"GCVEEB":{"16:9 Widescreen":"ADA686C90189D1D7D23E23E525EFC96EFA104BB9"},"GD7E70":{"Deinterlacing Fix":"6F8B6E33214162E5CDEBD2BC6A60FAFA04D2EB10"},"GD7PB2":{"Deinterlacing Fix":"3B798C9D63E755349B253FAF9186BAE1F703D96A"},"GDREAF":{"Fix audio issues":"F8EAE60FEB0CFB4477FDC4B9E136B63F68DFA63A"},"GDRP69":{"Fix audio issues":"E23D98B2CE185C3993A40F2495D37E41B971BF91"},"GEDE01":{"Fix startup hang":"21068C3CE905FB0CFFAA7408A93154AF8A5295A2"},"GEDJ01":{"Fix startup hang":"7061F3CF11BF64D3BA7F32CCF2BAC42FF3614AB6"},"GEDP01":{"Fix startup hang":"6F1B00517CBA30BEB738EAA90E71221378CD570D"},"GEME7F":{"Force Progressive Scan":"CB04E00918C9C0F161715D21D046ED6620F7ADEF"},"GEMJ28":{"Force Progressive Scan":"41D8F2C833C0B7B79FCA038752052059207AE4D5"},"GEZE8P":{"16:9 Widescreen [gamemasterplc]":"5CEBCFBEA4E444495D2FD6D8B6607DFB2349CC1B"},"GF7E01":{"16:9 Widescreen [gamemasterplc]":"C59CCA3EF8A5E97B32EB64DB9AE80E652ED281C6"},"GF7P01":{"16:9 Widescreen [gamemasterplc]":"1230053B5C347B62E432EFD6635433A183D18619"},"GFQEA4":{"16:9 Widescreen":"5CD9568CE58EF82EB371594667015C9B799454B9"},"GFZJ01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GFZP01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GGTE01":{"16:9 Widescreen [darkludx]":"EF128E7A9C22676834F558BA0F0F7FD8B9028727","Experimental 60fps":"D40344111D989EA009901F8B1C45B5AC8D39E6D2","Simple 60FPS":"5232A937D1D813FF58DD71D716284ED6AB535689"},"GGTJ01":{"Experimental 60fps":"BEC2310911003AF9E4B984A4051E0F885B8CC825","Simple 60FPS":"D62E777A2F019D7BD0AFFBCE876BD9AE408F1667"},"GGTP01":{"16:9 Widescreen":"FA9CD330ECDA01275DA88BD0803DE47757D16A4E","Experimental 60fps":"EB1ACD613BB432A3391CFFF1F0145A6ACE66B210","Simple 60FPS":"43FAD0DDD130BE1E4C8C7603EF6CA7DAA0DF5EE9"},"GGVD78":{"16:9 Widescreen":"9D07DBB5EC2FAA47F2E4587FBD75FD6F3E7E91B0","EFB Copy Fix":"FE52240DF6D132C15A8324E8A477F2BF2250D208"},"GGVE78":{"16:9 Widescreen":"86E561452235BF88D41884558EF34F54CE0FEB48","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVP78":{"16:9 Widescreen":"F9B2108D833084FA36A53E00F1647A9579F847D3","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVX78":{"EFB Copy Fix":"740F2D1C01DA39D1760D96B03974A48E6F74578D"},"GHAE08":{"Fix audio issues":"9799AFF8463EC86C9230E31E2627E141F0C129D3"},"GHAJ08":{"Fix audio issues":"B45A8FC32D14567B8D6C95F303E00A72C0E1D344"},"GHAP08":{"Fix audio issues":"BC7F3CFC97593AA2055C370C175950DC478D2709"},"GHKD7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKE7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"9BFBDE9184BC05EBD545625DEE486E3AAB36B6A3"},"GHKF7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHKP7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKS7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHLE69":{"16:9 Widescreen":"742BFC79D8F0BA5D6215772B58F5A0EADD7BFAFF"},"GHQE7D":{"16:9 Widescreen":"520E5F718D3BD1291C55021776091E0DE0FB4822","Limit internal frame rate during startup (speed hack)":"F6BF450D22104FD2527122EA6E81B1F70E916B95"},"GHQP7D":{"16:9 Widescreen":"AAE14CDC13A0C082A850DED85417100A8C8A0687","60Hz":"884A33613AE8D916128E3FF39B3AA9F63DAADC58","Limit internal frame rate during startup (speed hack)":"BD475ADEF145AC9226514CD1B2D4D5F3BFE67964"},"GHSE69":{"16:9 Widescreen":"3538CDBD13FA939B7BA5F0F82253401AFDD1E2CD"},"GHSP69":{"16:9 Widescreen":"409754E2EBB6F05DEE1AFC647E25B15D462638FF"},"GICD78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICE78":{"EFB Copy Fix":"5BF55685B8867A85EAA9C86571309B17BF7DED32"},"GICF78":{"EFB Copy Fix":"85AABAEB9A59C4F96D9330A3B884F6D757DA1683"},"GICH78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICJG9":{"EFB Copy Fix":"969134EA21A160EBDA91C0870266E7D1707FDC43"},"GICP78":{"EFB Copy Fix":"13B158CF41F5412BC637F50644193D43CC3DA49A"},"GIQE78":{"EFB Copy Fix":"E15AA1E30D26E5735D68AAADE436E7B7E4A33A35"},"GIQJ8P":{"EFB Copy Fix":"FFFCB76E98DDB06A7BBBC0AA73C869C87EB787D6"},"GIQX78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GIQY78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GK2":{"16:9 Widescreen [Vague Rant]":"6B4C59C2A2C5D71C7A38C8513A76E6467E307A26"},"GK2E52":{"60 FPS":"8F2397930A50C3C176188526EB9612578F9CBCE5"},"GK4":{"16:9 Widescreen":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKB":{"16:9 Widescreen Region-Free":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKBPAF":{"60Hz":"8E4E0ABA0E6D102A33206F34F4DEE63159B3CB4F"},"GKDP01":{"16:9 Widescreen":"DF4657937DE730107B636C73E959277EB963D210","60Hz":"6740D7B914CC897AF50798C0AB391965294FD1E1"},"GKRPB2":{"16:9 Widescreen":"69C01A4F91D991CF866237FED94A138308FC104F","60Hz":"40D06A5C7A3D873ABC73DEDF573738C2E3E37FF0"},"GKWJ18":{"16:9 Widescreen":"696570101FA040778EAD310377C484C846D87430"},"GKYE01":{"16:9 Widescreen":"6520EF1B7D88F00747B120A3A010458602ED989D"},"GKYJ01":{"16:9 Widescreen":"47B8EF7D02831AA5C375C698797BF2D5475FEFF2"},"GKYP01":{"16:9 Widescreen":"69413C75036D2975716066E6574461B981FF0124"},"GLEE08":{"Fix audio issues":"7355F358CAC6F418D37E4C23E64F7867D46E4FC9"},"GLEJ08":{"Fix audio issues":"12B24A6D7389A2AC5AB75FC0BF8493E7661F2A73"},"GLEP08":{"Fix audio issues":"81BD39F5527552DE89E3B59BA86298900F0A3168"},"GLSD64":{"Fix freeze in opening cutscene":"5E2A73717BD66EF647846DD64C33BC80AD9B5227"},"GLSE64":{"Fix freeze in opening cutscene":"1CE78E7954415A44DF693C0BB879AA5A4FF059A3"},"GLSF64":{"Fix freeze in opening cutscene":"009B0C4AD80A9C28C987934D254C2C4AACC9A07A"},"GLSP64":{"16:9 Widescreen":"8E7A544C10E7A5E2F0304A0D2586879627EF6586","60Hz":"B67144E87B54246137142992A3BC83DC13BE68A7","Fix freeze in opening cutscene":"3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC"},"GM4E01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"78BE50F93E81A1972CA31ABC318949E366E82BD0"},"GM4P01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"6D950B6EEFD6D304E1A424355B74A595D32BDF2F"},"GMBE8P":{"16:9 Widescreen":"D12DE9E3941BCE6EB50B8DA92140E09AFC0104C8","NA Stretched HUD":"0151A6683E1FBFD20096807C54A6952C4A4BAD40"},"GMBP8P":{"16:9 Widescreen":"8A688981F33A2C53882EF08FCF20B88AD43EF417"},"GMNE78":{"Limit internal FPS to VPS (improves performance)":"DE38BC8493EA24F93B6D805F28746AA99C53C677"},"GMNP78":{"Limit internal FPS to VPS (improves performance)":"65FB9B430F7D58EBC76DAADE8CA9DBBAA32B8B51"},"GMOP70":{"16:9 Widescreen":"CE4D298616BD42E4D8F8936B325CD1FD7F5B956B","60Hz":"0CD2CC787A5FF551901E41C85C8AAC02017ECB89"},"GMPE01":{"16:9 Widescreen":"4F98592DB3DEE3857469A8D8605FAF4BD6F7C76D","QOL - Allow Digital Presses for Map Screen":"E232B27564E9AA0C32DE163C9C056317A7B2B12E","QOL - Automatically Advance Text Boxes":"59607671BFC4717ACAF9807BB7EB0D9F982866D4","QOL - Disable Advance on Results":"CAEA37F3FEF89400513353EB85875F2A3AF4C03E","QOL - Faster Boot Time":"F75FBBD838C5B84FF687962FBA9195B217DE132C","QOL - Increased Board Speed":"19F264DE7F07EAC9433CA4B591BEBA1EC976C9F1","QOL - Increased Taunt Capabilities":"C70391D4961A0E820DE40141C89341369A9F021B","QOL - Instant Text Display":"F4E6913CF034E5778B9F9CA5FF448FF1B78B3333","QOL - Rumble Always Off":"93621947019532F02C25937FD3EEEE21A788CB07","QOL - Show Controller Port Number of Who Paused":"D7249AC3C949FEB860CF4350C5B7B79AC16569F1"},"GMPJ01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMPP01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMSE01":{"60FPS":"2805B1A0DD53EB64877D375D10F667700955720F","Widescreen":"BD718F961DBA5372B1D0257D454D535746C453A0"},"GMSJ01":{"16:9 Widescreen":"E8112A01040A06460E368F142C5D1FD0B4085D94"},"GMSJ01r0":{"60FPS":"9894B5B6B5215844D48411021FF8EECFE851D79D"},"GMSJ01r1":{"60FPS":"2DB69DB7A3753D543CD85CA4C77B4EF0AED7486E"},"GMSP01":{"60FPS":"D07009B710B3BBC6B12E54ED6A7969D58197EDAF","Widescreen":"0B7B89BF0868393076EF5F9DBD689DD0EDBCB84C"},"GNHE5d":{"Nop Hack":"89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF"},"GOCE5D":{"16:9 Widescreen":"E4D800B90F16A15D9252EED1B2A23AEAF5CD230A"},"GOME01":{"16:9 Widescreen":"C33CBBF6F22195BF626104B4827200B3EE1CF30C"},"GOMP01":{"16:9 Widescreen":"874784CCD4AC3DC9C69654E471C18AD0F5E4D9AE"},"GONE69":{"Widescreen Hack Culling":"6412A67E1C26E146A9526AFC9F0EE517B214069D"},"GP4J18":{"16:9 Widescreen":"BF4D2D6AF66F285E81173B3B20D5369DB6FB351B"},"GP5E01":{"16:9 Widescreen":"5ED053787332DB5EEB8B963BA5DF197B58C6BB75","QOL - Automatically Advance Text Boxes":"41EF84663008668682BB75DC8B874E38669AE041","QOL - Disable Advance on Results":"6C36BEFE751131298BC8B39012892F6A6903CE60","QOL - Faster Boot Time":"8E952E6E12639AC20F6F9FDD150C12F0C294DF43","QOL - Increased Board Speed":"4BD72B8ED75ED8EFB3329C104BAD114FDDB8AD2C","QOL - Increased Capsule Throwing Speed":"C5BA3C9D386D09DD6CB9242A12A0D041BEB93160","QOL - Increased Taunt Capabilities":"C360EBDDCB0237076052C983BD9719411CA67CDE","QOL - Instant Text Display":"2CD4A06A3B9C18CC9EA2A2DEBD96E066CD1D700A","QOL - Show Controller Port Number of Who Paused":"5BA3648D18346EDC97B23475D4748C637E6095A2"},"GP5J01":{"16:9 Widescreen":"81E69B6BEE85E06805EC24E1CFEA9405BF459DF8"},"GP5P01":{"16:9 Widescreen":"FD4FCCB97C06F0173A30D4CC9A99422B2C0ABA9B"},"GP6E01":{"16:9 Widescreen":"B502AF0887792E9B140516D14BA75F99772C17BE","QOL - Auto Advance Text Boxes":"E645DF70CE0266D174D917A817A7FBEEE974A9F0","QOL - Disable Advance on Results":"798E566B621F6A32BF23C588E76EAC0776E2A8A3","QOL - Faster Boot Time":"B089A3C80D5DA86ABF6258F8BFB1DC78BB54A973","QOL - Increased Board Speed":"501C52ACF117950888A66DB2BEDBDFD7D9B20235","QOL - Increased Orb Throwing Speed":"845F439278DA8CBC225658FF4E2F82D707422BFB","QOL - Increased Taunt Capabilities":"0AED27341A06186AD92D5725C0B9693CA8F85B7B","QOL - Instant Text Display":"5AAC4E21927D4AEE7AC329CE793D4CDAEDDF7A3F","QOL - Show Controller Port Number of Who Paused":"DF5D2F068D76ABF2B1C294D413B9154B8C356929"},"GP6J01":{"16:9 Widescreen":"88F50F8298F82E3E5C161277BD4A985CE893A9D0"},"GP6P01":{"16:9 Widescreen":"DCCE59CA987624C753F0A86BEBA4287635901C93","Remove Black Bars":"138A86D43B5830BFE1926F58C0A4FBE2971BB02D"},"GP7E01":{"16:9 Widescreen":"87EBF72D90503EBC5CE9BC994795669E48A92A4A","QOL - Auto Advance Text Boxes":"82338AB5D82A7F398B4EB674CA4866DD1F4A1E65","QOL - Controller Options Always Acesssible":"D2F9CC0B9E52136E77187395FB7DA9B64B2C3B30","QOL - Disable Advance on Results":"A644551162A61DE7997B2A84F5BFDEFFD3FC2FB5","QOL - Faster Boot Time":"1939B5C575AE8FF6890BDDDD1B3FEA472D8CABA0","QOL - Increased Board Speed":"7E06A2EBCFECADB8074152B489984237A6FE9F21","QOL - Increased Orb Throwing Speed":"6671BEE71920D6B845AE1257AFE3AB3B95CD6FBD","QOL - Increased Taunt Capabilities":"9F60923F95FFE709CCD06966CD2DF743263934D1","QOL - Instant Text Display":"48FA6002E42DA9BB83ABC980A32C60CE3B21C4FC","QOL - Show Controller Port Number of Who Paused":"5E6F8C6C87D5944DB18A4E4F9A45F61EE87A4163"},"GP7J01":{"16:9 Widescreen":"A081F0729068D233E99DC00C64E36CA33C945640"},"GP7P01":{"16:9 Widescreen":"75D6CDA4EA301A71F7701A0487544E79136850B9","60Hz":"6D0D5B7CA58C38A3147F8ED981B5B21CC6C519CD"},"GPIE01r0":{"16:9 Widescreen":"2B58166A66644F984E64077A6245C94C79B51063"},"GPIE01r1":{"16:9 Widescreen":"9A1545AB4E2B9216B95AFFA83420D723512DA8C8","60FPS":"3337C767EAA21D30C6CFEAE9985FFCABFC63E2E3","Disable Pikmin optimizations":"CBD15E9104929F0631713AE482475CB2B90E71F2","No Blur":"A25CDDB79991F090BD7F12840AD06F13BD5CCF95"},"GPIP01":{"16:9 Widescreen":"5AE44D5B0E0D4C412B453B95CF5A41DBA4D685E6","60Hz":"AE59FF751E167632AE283F38CC4B96BDF7B9B81F","Turn off blur":"4ED79A548DBD7DBD35928A2F1138315FF103E260"},"GPNE08":{"16:9 Widescreen [Ralf, darkludx]":"ED3D9BB08C5F1D21BC8D5C73155C00E3C25C466F"},"GPNP08":{"16:9 Widescreen [Ralf]":"469B18FE8219031D355557B836EF02B625040A4F"},"GPOE8P":{"Make Save Copyable NTSC Port - 1.0\/1.1":"7A4B3E596BA4FDA8F3FCF984079B3F44CB2D6AB3","Make Save Copyable NTSC Port - PLUS\/1.2":"304DB311F2107E88EBCE058535B266F9263AA8E2","Save Validation Code v 1.0\/v1.1":"9F6259132453DCC0607EAB35DB182C76818F2F9F","Save Validation Code v1.2\/Plus":"17EB6CFB408EF27D44C053A1336C3B87B6A05018"},"GPOE8Pr0":{"16:9 Widescreen":"99986BB1D4ABE45C429D48B01BFB578FEA69C14F"},"GPOE8Pr1":{"16:9 Widescreen (Plus) [Ralf]":"5563671A6702785FFE106DAB7EF3F4FE6D9F705B"},"GPOP8P":{"16:9 Widescreen":"DF133C61C29D5CFA87A2A785C962C205F80E6D04","Game Save Valid":"40EC1590DB26C7C58D00E3065F1E404341EF0E73","Game Save copyable":"2F5FB98927DAFF141D1CCD1EACA8AF626397E284"},"GPVE01":{"16:9 Widescreen":"E5B51080CFD518FE584CF3B61A1099CD032479D6","60 FPS":"6662E99E41F77763EEE524477DF341836EC7032E"},"GPVP01":{"16:9 Widescreen":"78F3C9A789827B063A6701987262276A66ABD82B"},"GQPE78":{"EFB Copy Fix":"880B114E9A308084CAB92C004A9EE067B371C310"},"GQPP78":{"EFB Copy Fix":"5D9A14954AE8D639C9B254F3BA73A70F284BBC8D"},"GQSDAF":{"16:9 Widescreen":"CA82C44B2E8FC5C184E3FF935BC89661B7DB55D6","60Hz":"952165FD78543EB6D2F5230F7570B0513773D332","No Blur":"DEB7DF358423F0EF30C9FD49F4F14590836D52A6"},"GQSEAF":{"16:9 Widescreen":"48AFE1D32843C6B5AD4337A49F15FD447DF2A752","Remove Blur":"EF448CF1FA6A15EB2661EA5338703C13D52EF65B"},"GQSFAF":{"16:9 Widescreen":"76C30E1737CC5C3B5DE32532B5F9A5CD50B690F8","No Blur":"986BAE5103CDE9286D2D34FD28FE2540D0759AAB"},"GR2E52":{"16:9 Widescreen":"5F55CE7DA7E1E3261CF2FF88F34CDA63BA511003"},"GR2J52":{"16:9 Widescreen":"D3C6FA690BED07BFA65C2E1FCDE2FFA5AA0EEEE4"},"GR2P52":{"16:9 Widescreen":"D7BCADD4E76B5E3F7D9D463F55A2777528E37E94"},"GRNE52":{"16:9 Widescreen":"483D74C47AD1012606D591A226AB3144C7FB201C"},"GRNJ52":{"16:9 Widescreen":"949281B7221B20680B7BC29E4754B73A326EBE85"},"GRNP52":{"16:9 Widescreen":"11051B094FE5A8B2E73060BFF786C1588E36979A"},"GROE5Z":{"16:9 Widescreen":"FADBAB5EC280CF8ED4C9536A33F4184BA210C9B0"},"GRYE41":{"Disable Culling to Fix Rise and Shrine Hang":"AF0A575EB6071EAC0D2EC3D2EA30A23EB05A4192"},"GSAE01r0":{"viWidth 704 Aspect Ratio Fix":"0D529180B9B28BB06E21EDBDBA61C17EEF0FFB7F"},"GSAE01r1":{"viWidth 704 Aspect Ratio Fix":"CB15ED22625690A5DBE4607FE30160125421461D"},"GSAP01":{"viWidth 704 Aspect Ratio Fix":"749EDA62B1B73354599929F1B50E992E111C5493"},"GSNE8P":{"16:9 Widescreen":"CFDF3FA5B48B347E49E8E521D7BE47FCC9A5CBF4","60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSNJ8P":{"60Hz Aspect Ratio Fix":"98DF9D67DE77F66A4B09F25C5621805DEB40865B"},"GSNP8P":{"60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSO":{"16:9 Widescreen Region Free":"0D60D0593F6DA28B1236381B22082506B9533F0E"},"GSPE69":{"16:9 Widescreen":"80E18A8963F4EFD3C03494C95934452FCB6E521B"},"GSPP69":{"16:9 Widescreen":"162E87E9F5511CD82216811055836C11B374C45B","60Hz":"E5DF2DF34D2F0BDEE4205C891576511EC572C5DC"},"GSWE64":{"16:9 Widescreen":"6480326CCA8FF64A4868EC6E9FDE9F38819A75A1","Disable Dithering":"85F65E4B627641862C1A6091D29A3BB0626B04C3"},"GTEE01":{"16:9 Widescreen":"6B57EBCB9CC0ACE2EDA0F13E3F1F55F3FABC3A26"},"GTEP01":{"16:9 Widescreen":"72785784363C46180AF8E388348FD77667E7D382"},"GTOJAF":{"Remove Blur":"53A2EDF113F2ED7E54A16AA0E73FDB2C44C79DE5"},"GTRE78r0":{"Limit internal FPS to VPS (improves performance)":"42B85EFC5DEF8B243521EAD434DDCBDF95664CB1"},"GTRE78r1":{"Limit internal FPS to VPS (improves performance)":"ADB3A41B992EAB15AE484B2E575ACF3354415186"},"GTRJ8N":{"Limit internal FPS to VPS (improves performance)":"47DBEFDCF22ADC2AB148A54AC885FE19B3C07E3F"},"GTRP78":{"Limit internal FPS to VPS (improves performance)":"E8752E1B60DF76CD0CC7408074E742B0966BC904"},"GTZE41":{"16:9 Widescreen":"ECCE87E0006475C73E7D936AA0150BD20166F9EF"},"GTZP41":{"16:9 Widescreen":"62D8F159B93167171E0860D96C4BB32A5BF5648B"},"GU2D78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU2F78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU3D78":{"EFB Copy Fix":"8A0E3114862ADFE421874211BD6F5220AA425BF5"},"GU3X78":{"EFB Copy Fix":"E3303FDAE7ECA17A72EDC440C32D94648A6453A0"},"GU4Y78":{"EFB Copy Fix":"D54767785E139A8BC8C4B75573FBD5A0B686D8E3"},"GUNE5Dr0":{"16:9 Widescreen":"C942EBCBE8A487C25E296EC1FAC2358DA1487DBD","60 FPS":"A441630EC5FF0EB74D2243A3092D22E69C6BEA02"},"GUNE5Dr1":{"16:9 Widescreen":"39BD84CBE2AFACFEDEC5E9020D1A8B0D36DD53F9","60 FPS":"05779F1A3D0C43305D52A95447D612CB424942C0"},"GUNP5D":{"16:9 Widescreen":"FE3BA1DAA1AF278A7839E27BF466B0BA0B390EC2","50 FPS":"3B20499A3F8D1D7CCA5B0015F2D80ECED25842A6"},"GV4E69":{"Fix 2D Rendering":"8679891FCAA250FCFF670B26E0CB9875900D17FD"},"GVJE08":{"16:9 Widescreen":"09EFDA75D876A675121C8344D8FDD09A70A1A846"},"GVJP08":{"16:9 Widescreen":"93938F4C6112C91549F2B52E6259170F7501EC07"},"GVPE69":{"Fix 2D Rendering":"3159CA79B0A890131763EA6CB163684BEE886E3F"},"GVSE8P":{"16:9 Widescreen":"4CE9C491160A4B631142EE9CE802C694163F1CA3"},"GWRE01":{"16:9 Widescreen":"AC42770B06662BE1DC863EC80F44B5E034C63664"},"GWRP01":{"16:9 Widescreen":"9DDDFAB28C4BD35CF64050E1EF684DC042B1AFFA"},"GWWE01":{"16:9 Widescreen":"98B2E75D8E1CED4A964D3129A5DC10E30538CAA6"},"GWWJ01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWWP01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWZE01":{"16:9 Widescreen":"9EFC191DE6D21A1681FE241AB2EE4A131259F317"},"GXCE01":{"16:9 Widescreen":"901A1E78A3A0124F55548507D3B3707125C64A8A"},"GXSE8P":{"16:9 Widescreen":"F50BBA440184FC77A4DFFAA58FF2BB888E2E072F","Aspect Ratio Fix":"4214C74DFB8A74FC3AA4A643E869BC4D9A38EDA0"},"GXSJ8P":{"Aspect Ratio Fix":"227909607984BBC3D36AAC7DB9DFE385F3363C49"},"GXXE01":{"16:9 Widescreen":"7CAFDAD7B5E7459CCC1BB209D439DDCBB5BE4E4E","60 FPS":"7E2829049003FC9DC8BC28044B33CAFF3768B54C","Allow Memory Card saving with Savestates":"64FAA15062F0D0C319F904BBDE9C4489A25D6369"},"GXXJ01":{"16:9 Widescreen":"AF704A8C6838FDF47054D9EDD9106B3F46781FA9","60 FPS":"4F5FD45B4EC1B33FE076FA06F8FF863C46A06BBA","Allow Memory Card saving with Savestates":"8293802260536FA2EF2EFDAB5266DE36BB88DE1B"},"GXXP01":{"16:9 Widescreen":"02716B2585BEE74F8FCDD97A78F6A0D3DC7F331B","60 FPS":"BF6171EA5CCF4B57AA596372589DDB5E6904DD7A","Allow Memory Card saving with Savestates":"3CAFBC4AE6FC5CE9F53377F86AB5BD8F1BC8861A"},"GYFPA4":{"60Hz":"402ED10AC842041AB4B39AE8F2D81B2D7AEF9CB4"},"GZ2E01":{"16:9 Widescreen v2":"E7521ED27BFC972628906CBE8D5403ED57253BB8","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2J01":{"16:9 Widescreen":"F985A0A58D8E2B23E8A557FAFF8D367AFFEADD07","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2P01":{"16:9 Widescreen v2":"CAC38B0D334B925A57AA3118D35932B8A185137E","Hyrule Field Speed Hack":"0F63623D4D984B7706F718F57C0ABDB6DBADCF8D"},"GZ3E70":{"16:9 Widescreen":"167F9E9A9B372CB3A01F308B46FF1403F8599C51","Deinterlacing Fix":"EF1291C0E45277CDAE4D0F61E1949C1E75E5A3E1"},"GZBJB2":{"Deinterlacing Fix":"19D67BD12A18F0E3A960BBB92E2F490BEF5C00C1"},"GZDP70":{"16:9 Widescreen":"2E424E0BBAE6EF5D6A8FB4224EA1D0746BAC37D3"},"GZLE01":{"16:9 Widescreen":"9FA864EE7DD8CE7FF538EB4E0243F20137430BD8","Remove Distance Blur":"78EA34CEF8E01701491C280F155F1C12EC9BF1A2"},"GZLJ01":{"16:9 Widescreen":"E03B61989025CA33937E63A057E6E40A403811BC"},"GZLP01":{"16:9 Widescreen":"3EFFF6C52B5633A1729FAA6883D8579E77F7D057","Remove Distance Blur":"14BECBEA4DD281EBD0F7FE7DEE8020B6F2418ACD"},"GZPE70":{"16:9 Widescreen":"7142F2495507AC7136992128ED0FCA6BC72B61F4"},"GZPP70":{"16:9 Widescreen":"591FD6C9668C79FF1CE8558EBED2486A7A327F05","60Hz":"A59B84DB5486521AEE1C23B6C741ECD35216E5AD"},"GZSE70":{"16:9 Widescreen":"8E86EAC7EA4F4D2854DD9020CD795630CA64C4EE"},"HAF":{"BufferPatch":"181195871F63B89B1CF09AFA4420CF89B9883108"},"HAL":{"RSAPatch":"AD12237401ABE9FE4A545AADB5C5AE10355E2076"},"P1RE01":{"60 FPS":"FD15A2EC38E46C1C3153336BC44F11B08ED906D0"},"R7XE69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"R7XJ13":{"Speed hack":"4590D425BCBAFA0D1E32D27B46068843F6A2BE09"},"R7XP69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"RB7E54":{"Limit internal frame rate in loading screens (fix hangs)":"BD1A252B43FD339391ED33554542E786BAD6671B"},"RB7P54":{"Limit internal frame rate in loading screens (fix hangs)":"90247F89DF8EB7A4AFA2C425CD3006257A95A8F8"},"RGQE70":{"crashfix":"5F4CF8D4DA19A0FF74FF9EB925AC0236069BFD59"},"RLEEFS":{"Fix crash on main menu":"793642AC6862C2F3412035A9E3D7172CC4A1D5C7"},"RM8E01":{"16:9 Widescreen":"E61344EB1542A78D497981C307B6549985C7A05A","Extra - Disable Music":"25223F9EFAABF601CAC7810004F124E4056598B1","QOL - Faster Boot Time":"BCC4279F8B28636AD773F01540E78DF40EAD6087","QOL - Increased Board Speed":"B65AF1819966CD3435D88801E8C79704E3A52DB5","QOL - Increased Taunt Capabilities":"66495D7CB532FAE778AFC22CF45D17D0FFDE5310","QOL - Increased Text Display":"B4186DDC54F33F4D6A22188EF50CEB43FB205673","QOL - Invert IR Stick for GameCube Mod":"9D90A9C66AE8AD91B201B40C4145D1323B701A77","QOL - Remove Explanations":"330DD53AB993A99576564FEFD222D7BD211B878F"},"RM8J01":{"16:9 Widescreen":"A140BDCB8E1721CB6B4CD878E412113322258B57"},"RM8P01":{"16:9 Widescreen":"F664C32AFD3D785FC6E04D8990A3FA1C72A18C5C"},"RMHE08":{"Bloom OFF":"CCF233DA57B3E75221870DE502955114B0D4E7FA"},"RMHJ08":{"Bloom OFF":"29D3625B7ED577587E56AA07CB0EB8C47C97E823"},"RMHP08":{"Bloom OFF":"1720C1173D4698167080DBFC4232F21757C4DA08"},"RO2P7N":{"Hangfix":"EEE9C8DE4671C18DD7F81DD08D39B64C57600DEA"},"RPBE01":{"Fix black screen effects":"775ABECA6073E02C5C68CF4D644194D966A418F5"},"RPBJ01r0":{"Fix black screen effects":"0EAB5D8DE827894AFEF97C10ACB67378E6983323"},"RPBJ01r1":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBJ01r2":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBP01":{"Fix black screen effects":"82AEB60F9A9083F93060531A970FFAABE0833A40"},"RRBE41r0":{"Idle loop speed hack":"20C7A2D4A3D6D55A8029A9D200D57CBD11F60B88"},"RRBE41r1":{"Idle loop speed hack":"0A30FA4C7E787197A00B7267368F372EFF623776"},"RRBE41r2":{"Idle loop speed hack":"2227787007F16F0CD454422AE6EB94656464B6D0"},"RRBJ41":{"Idle loop speed hack":"E9209EB728AA13375B2ABEF1C15D066DAFBF2EBF"},"RRBP41":{"Idle loop speed hack":"8A47BC311138EE3A11E049DF3EE6AED38990FB49"},"RTH":{"Disable blur":"812EE46AC967BFCD239335B10A664D71A93E8175"},"RUUE01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"B62652D5BBE0701900090CFEB32003AE6E5A5015","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"244D1FC7D6284DC8832CF7F34506B0DE4DBF8914","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"6FDD09D51EC3D754BA7F80630B1FE1D3FEF6FE97","QOL - Disable Grass Deterioration":"D566F2AF5BB1EDB8284461DEA85FF3B5A8DEBDD6","QOL - Enable Fast Text":"C7D8A7926ED952A2797DA312B41B08DCCDC3A274"},"RUUE01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"FC7BF1D6A5B4ED39EA4671C241E78F3BC8037E97","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"0235631B6F9A439B9718635F5AB7ADAFA2B6B3DE","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"5DEAC5F3390CE19487A9473A1988E7BB5EA4790F","QOL - Disable Grass Deterioration":"0FE1EF4DE5C8AE691E96634BC66BA034850C6C0F","QOL - Enable Fast Text":"B2BFF779434FDBEF08FFD73C4B140FCE0A8FDF0E"},"RUUJ01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"D79F773E1363B694784C94DD309709D41D368A08","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"C88CFD7E6C18F570DE974D6C0202987E4F1B72C4","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"3C92010E7DE2E93C48AFF25B30A18BBFCBD12707","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUK01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"1B89E8A6E6E09757373FF116BBC0989C06651709","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"EF883FB0C237BF51ABD88C35D34571D240C79484","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"F08D0F233F3F5833DFAFE5C8049C37D32360F0FA","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"C55A5CDEF4C868E66C1FB2902D3285B2C350D7F1","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"D28D4E4FBD153A6D4C49E2D250213F37BEC941E5","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"80C3F50C018E076DF2CD39732A5ABB8A1C0E88B1","QOL - Disable Grass Deterioration":"3B278C19AC76D27488C55201BA0DB63864F66AAF","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"00D994940AD323786A9DA780D1347AE5FBABAC8A","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"9121BEDD72436F4EAB47E4CE96ABD864118B22F3","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"8FC58B3DC4817515822EBB82BB7A8055270EE51D","QOL - Disable Grass Deterioration":"9B199D77AF748518358D32966F3F55F5C5759C3A","QOL - Enable Fast Text":"827CF5DFC90A258C36BF32DC3883371973E0B706"},"RX4E4Z":{"Fix file reads (dcache bypass)":"9E4E0F1465A9A1E85349DBA3B1278AC215A97DBB"},"RX4PMT":{"Fix file reads (dcache bypass)":"EE85907C03F0295794821383B93F8D5B91D2697A"},"RZDE01r0":{"Hyrule Field Speed Hack":"15EAD073414C9903D6CAE5229DCE582BD17A9162"},"RZDE01r2":{"Hyrule Field Speed Hack":"27395CC8BC2C51201D566657D31A471A850482FB"},"RZDJ01":{"Hyrule Field Speed Hack":"B3F7473F8C911A32F1D616491C9E78EBBD7A6309"},"RZDK01":{"Hyrule Field Speed Hack":"A280C0114B800D7DC056ECFB5E482229DA0B1550"},"RZDP01":{"Hyrule Field Speed Hack":"2A83ADFB760F9498841ED0ED68B0C0438232472C"},"SAOE78":{"Fix crash on boot":"EA11FA4908FB20B61876ACD360EC7657A6D39FB2"},"SAOEVZ":{"Fix crash on boot":"AA55C214DE7545DE0E203CC39F06BF3D31451BE9"},"SC2":{"Limit internal frame rate (speed hack)":"B3FB0FCE108E85E6198BAF3535850470049E2BE2"},"SGLEA4":{"Fix black screen":"258378187ACF475A55EFEAF8A703681252E014C3"},"SGLPA4":{"Fix black screen":"6F8CD59D897338CA90939149E1A62588620C6D88"},"SX4E01":{"60 FPS":"206E79C5E9B5E07CEB7F52D0F5E5FF3400009265"}} \ No newline at end of file diff --git a/Data/Sys/GameSettings/SX4E01.ini b/Data/Sys/GameSettings/SX4E01.ini new file mode 100644 index 000000000000..450c8f85da0c --- /dev/null +++ b/Data/Sys/GameSettings/SX4E01.ini @@ -0,0 +1,230 @@ +# SX4E01 - Xenoblade Chronicles + +[Gecko] +$60 FPS +# Author: One More Try +# Forum Thread: https://forums.dolphin-emu.org/Thread-ntsc-jp-xenoblade-60fps-and-other-mods +# Notes: +# * Requires a CPU Overclock of at least 110% to maintain stable frame rates during battles. +# * May cause intermittent crashes in some areas; disable temporarily if a consistent crash occurs. + +# Run once when apply +A8000000 00000000 +# Camera speed optional +C206C36C 00000003 +EE81082A EE81A024 +EC210532 EC000532 +D00D82BC 00000000 +C206C384 00000004 +EC420532 EEB4A02A +EC150028 EC000532 +EC150028 D04D82F4 +60000000 00000000 +# Melia effect fix +C24CD770 00000002 +C022CF74 EC000828 +D0170018 00000000 +# Affinity screen fixes +04668860 42080000 +04668864 41900000 +0466886C 41200000 +04668868 C1200000 +# Gem crafting animations +046684DC 42200000 +046684D4 42C80000 +046684D8 42700000 +# Condition cycle speed +04666fdc 42F00000 +04667038 42F00000 +# UI slideout +046673f0 43340000 +04667420 43340000 +# Stairs unsticky +C24C1ED4 00000002 +EC3F0024 EC2100B2 +60000000 00000000 +# Buggy lift +0466606c 3b23d000 +# Burst B fix timing +04667870 43480000 +C2181264 00000002 +EC0010FA EC00182A +60000000 00000000 +# Ice Slide Speed +04666090 3F7D6A16 +04666098 3F400000 +0466609C 3F7DD000 +# Slow Text +0412e66c 41820008 +C212E668 00000003 +7F85E378 7F66DB78 +3E608000 82731824 +# Text End +2C130001 00000000 +# Slow Cutscene Text +041a3dec 41820008 +C21A3DE8 00000003 +807F0088 7FA4EB78 +3E608000 82731824 +# End +2C130001 00000000 +# Cutscene Switch speed +0466AB90 3F000000 +C24A0838 00000002 +3E808082 82949E68 +2C140002 00000000 +# Screen dimming duration +C203A52C 00000004 +3E808082 82949E68 +2C140001 3803F800 +41A20008 3803F000 +60000000 00000000 +# Head Movement +C24ED0AC 00000001 +ECC0C63A 00000000 +C24EC8BC 00000001 +EC40FFFA 00000000 +C24ED0B4 00000001 +ECA0C63A 00000000 +# Menu 60fps +C249A358 00000004 +3FC080CA 63DE54B4 +C282CF74 7C03F000 +41A20008 EC210532 +83C10008 00000000 +041C3904 3803FFFA +041C38EC 2800001A +041C3890 2800001A +041C38A8 3803FFFA +046687A0 41A00000 +046687A8 40666667 +04668890 42080000 +# Menu End +04668894 41900000 +# Select art controls +C24497D4 00000002 +7D084214 7CE83A14 +60000000 00000000 +C24497FC 00000002 +7D084214 7CE83850 +60000000 00000000 +C2449844 00000002 +7D084214 7C083800 +# Controls End +60000000 00000000 +# Grass double value if fraction +C20BAD8C 00000003 +EEA1082A FE810878 +FC34A86E C01F2F48 +60000000 00000000 +# Moving Texture +C23DFE1C 00000003 +1C00000C D022FFFC +F0223FF8 E0223FF8 +60000000 00000000 +C23DFEBC 00000002 +C043000C C022FFFC +60000000 00000000 +C23E5B54 00000004 +C02D9BE0 3D808082 +818C9E68 2C0C0002 +4182000C C062CF74 +EC230072 00000000 +C23E5128 00000004 +C02D9BE0 3D808082 +818C9E68 2C0C0002 +4182000C C062CF74 +EC230072 00000000 +C23E8AEC 00000004 +C02D9BE0 3D808082 +818C9E68 2C0C0002 +4182000C C062CF74 +EC230072 00000000 +C23DFEB4 00000012 +C062FFFC FC030840 +C0230004 41820080 +C0630000 C043000C +EC621828 C042C010 +C0A2CF74 FC021840 +41A20038 C023FFF8 +C0630004 FC801A10 +FC000A10 FC040040 +41A10014 FC801050 +FC8120AE EC64182A +4BFFFFE4 EC230828 +EC25187A 48000030 +C0630010 FC801A10 +FC000A10 FC040040 +41A10014 FC801050 +FC8120AE EC64182A +4BFFFFE4 EC43082A +EC220172 00000000 +# Timer FPS Multi +C244B714 00000011 +# Gr1824 +3E808000 62941824 +# Extra Speed Multi +80140000 68000001 +90140000 3E808066 +82944268 2C140000 +38000001 3CA03F00 +41A20028 3E808066 +82943E18 3C008082 +6000CB00 7C140000 +38000001 41A2000C +38000002 3CA03F80 +3E808082 90149E68 +3EA080CA 801554C0 +2C000000 41820020 +3EC08066 80163D24 +2C000001 40A20008 +3CA03F80 90B554C0 +38630001 00000000 +# Timer mods using Gr1824 +C216ADC4 00000005 +# skip if movie +3E808082 82949E68 +2C140002 3A800001 +4182000C 3E808000 +82941824 7C04A214 +60000000 00000000 +C216ADCC 00000001 +7C03A214 00000000 +# Not Skipped +C21669CC 00000002 +C242CF74 EC0004B2 +D01E01E8 00000000 +# End Timer +60000000 00000000 +# Speed Multi +C249A374 00000005 +EE739828 FC019840 +41820018 3E808082 +82B49E68 2C150002 +41820008 C022CF74 +D023000C 00000000 +# Intro Only +C216AD8C 00000004 +3E808082 82949E68 +C0229BCC 2C140002 +41820008 C022CF74 +60000000 00000000 +# UI Speeds Half +C2137DE8 00000002 +C002CF74 EC210032 +C0030010 00000000 +# Climb Fix +C205C414 00000002 +C3E2CF74 EC01EFFA +60000000 00000000 +# Run Speed +C244B460 00000001 +38000002 00000000 +# Check once +22CA54C0 00000000 +# 60 FPS speed +04CA54C0 3F000000 +E0000000 80008000 + +[Gecko_RetroAchievements_Verified] +$60 FPS diff --git a/Source/Core/Core/AchievementApprovedHash.h b/Source/Core/Core/AchievementApprovedHash.h index b0508f399f2d..ff137737a32d 100644 --- a/Source/Core/Core/AchievementApprovedHash.h +++ b/Source/Core/Core/AchievementApprovedHash.h @@ -9,5 +9,5 @@ static constexpr std::string_view ACHIEVEMENT_APPROVED_LIST_FILENAME = "Approved // After building tests, find the new hash with: // ./Binaries/Tests/tests --gtest_filter=PatchAllowlist.VerifyHashes static const inline Common::SHA1::Digest ACHIEVEMENT_APPROVED_LIST_HASH = { - 0x75, 0x4E, 0xFB, 0x44, 0xBB, 0x97, 0xD0, 0xF5, 0x62, 0xA0, - 0xCF, 0xEA, 0xC3, 0x0E, 0x93, 0xA3, 0x87, 0xF3, 0x7F, 0xB5}; + 0x0B, 0x47, 0x3F, 0x5A, 0x84, 0xC0, 0xC1, 0x6D, 0x08, 0x88, + 0x05, 0x5C, 0xC6, 0x17, 0x60, 0xED, 0x02, 0x3C, 0x75, 0xF0}; From 896ecddb7751e0b1efee8d0ddb45d8a2c95aa0d4 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 7 Jun 2026 11:01:04 +0200 Subject: [PATCH 081/270] GameSettings: Remove SafeTextureCacheColorSamples from gbihf Extrems said that it's unnecessary because GBIHF doesn't use the GPU. Video output doesn't work in GBIHF in Dolphin regardless of this setting, so I can't actually test it. --- Data/Sys/GameSettings/ID-gbihf.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/Data/Sys/GameSettings/ID-gbihf.ini b/Data/Sys/GameSettings/ID-gbihf.ini index 968108e29e4c..cd829d08375c 100644 --- a/Data/Sys/GameSettings/ID-gbihf.ini +++ b/Data/Sys/GameSettings/ID-gbihf.ini @@ -6,6 +6,3 @@ DSPHLE = False [DSP] # Make sure we get DSP LLE recompiler and not DSP LLE interpreter, for performance. EnableJIT = True -[Video_Settings] -# Fixes old frames being shown when a game only updates a small part of the screen. Examples include gradually revealed text and the HP bars in Pokémon. -SafeTextureCacheColorSamples = 0 From 7ce4db1c7d9e41cee78954c758fcad3c419baee3 Mon Sep 17 00:00:00 2001 From: VampireFlower Date: Fri, 22 May 2026 20:35:57 -0400 Subject: [PATCH 082/270] [Debugger]: Assemble instructions in the patch instruction dialog --- .../Debugger/PatchInstructionDialog.cpp | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/Source/Core/DolphinQt/Debugger/PatchInstructionDialog.cpp b/Source/Core/DolphinQt/Debugger/PatchInstructionDialog.cpp index 9a54567d3ead..e00165943943 100644 --- a/Source/Core/DolphinQt/Debugger/PatchInstructionDialog.cpp +++ b/Source/Core/DolphinQt/Debugger/PatchInstructionDialog.cpp @@ -9,7 +9,9 @@ #include #include +#include "Common/Assembler/GekkoAssembler.h" #include "Common/GekkoDisassembler.h" +#include "Common/Swap.h" PatchInstructionDialog::PatchInstructionDialog(QWidget* parent, u32 address, u32 value) : QDialog(parent), m_address(address) @@ -49,14 +51,43 @@ void PatchInstructionDialog::ConnectWidgets() void PatchInstructionDialog::OnEditChanged() { - bool good; - m_code = m_input_edit->text().toUInt(&good, 16); + const QString input = m_input_edit->text(); - m_button_box->button(QDialogButtonBox::Ok)->setEnabled(good); + bool legal = false; + bool is_hex = false; - m_preview_label->setText( - tr("Instruction: %1") - .arg(QString::fromStdString(Common::GekkoDisassembler::Disassemble(m_code, m_address)))); + QString preview = QStringLiteral("(ill)\t---"); + + const u32 hex_code = input.toUInt(&is_hex, 16); + + if (is_hex) + { + legal = true; + m_code = hex_code; + preview = QString::fromStdString(Common::GekkoDisassembler::Disassemble(hex_code, m_address)); + } + else // Try to parse as a single instruction + { + const auto asm_result = Common::GekkoAssembler::Assemble(input.toStdString(), m_address); + + if (!Common::GekkoAssembler::IsFailure(asm_result)) + { + const auto& blocks = Common::GekkoAssembler::GetT(asm_result); + + if (!blocks.empty()) + { + legal = true; + + m_code = Common::swap32(blocks[0].instructions.data()); + + preview = QStringLiteral("%1").arg(m_code, 8, 16, QLatin1Char('0')); + } + } + } + + m_button_box->button(QDialogButtonBox::Ok)->setEnabled(legal); + + m_preview_label->setText(tr("Instruction: %1").arg(preview)); } u32 PatchInstructionDialog::GetCode() const From da45cbe8fd18229fe5ac9a1f3b81a1598a9ed4d6 Mon Sep 17 00:00:00 2001 From: Martino Fontana Date: Mon, 8 Jun 2026 23:13:50 +0200 Subject: [PATCH 083/270] JitArm64: Fix uninitialized use of register in dcbx Also do a little cleanup in the equivalent part of Jit64. --- Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp | 16 ++++++++-------- .../Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp | 8 ++++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp index 51fd33676051..89599f279e5d 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp @@ -326,8 +326,8 @@ void Jit64::dcbx(UGeckoInstruction inst) } } - X64Reg addr = RSCRATCH; - MOV_sum(32, addr, Ra, Rb); + X64Reg effective_address = RSCRATCH; + MOV_sum(32, effective_address, Ra, Rb); if (make_loop) { @@ -338,19 +338,19 @@ void Jit64::dcbx(UGeckoInstruction inst) } X64Reg tmp = RSCRATCH2; - RCX64Reg effective_address = gpr.Scratch(); - RegCache::Realize(effective_address); + RCX64Reg addr = gpr.Scratch(); + RegCache::Realize(addr); FixupBranch bat_lookup_failed; - MOV(32, R(effective_address), R(addr)); const u8* loop_start = GetCodePtr(); + MOV(32, R(addr), R(effective_address)); if (m_ppc_state.feature_flags & FEATURE_FLAG_MSR_IR) { // Translate effective address to physical address. bat_lookup_failed = BATAddressLookup(addr, tmp, m_jit.m_mmu.GetIBATTable().data()); MOV(32, R(tmp), R(effective_address)); - AND(32, R(tmp), Imm32(0x0001ffff)); - AND(32, R(addr), Imm32(0xfffe0000)); + AND(32, R(tmp), Imm32(PowerPC::BAT_PAGE_SIZE - 1)); + AND(32, R(addr), Imm32(~(PowerPC::BAT_PAGE_SIZE - 1))); OR(32, R(addr), R(tmp)); } @@ -366,7 +366,6 @@ void Jit64::dcbx(UGeckoInstruction inst) if (make_loop) { ADD(32, R(effective_address), Imm8(32)); - MOV(32, R(addr), R(effective_address)); SUB(32, R(loop_counter), Imm8(1)); J_CC(CC_NZ, loop_start); } @@ -379,6 +378,7 @@ void Jit64::dcbx(UGeckoInstruction inst) BitSet32 registersInUse = CallerSavedRegistersInUse(); registersInUse[X64Reg(tmp)] = false; registersInUse[X64Reg(effective_address)] = false; + registersInUse[X64Reg(addr)] = false; if (make_loop) registersInUse[X64Reg(loop_counter)] = false; ABI_PushRegistersAndAdjustStack(registersInUse, 0); diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp index 8c80151e91e5..4186765460a2 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp @@ -867,10 +867,14 @@ void JitArm64::dcbx(UGeckoInstruction inst) bat_lookup_failed = BATAddressLookup(physical_addr, effective_addr, WA, m_mmu.GetIBATTable().data()); BFI(physical_addr, effective_addr, 0, PowerPC::BAT_INDEX_SHIFT); + // Check whether a JIT cache line needs to be invalidated. + LSR(physical_addr, physical_addr, 5 + 5); // >> 5 for cache line size, >> 5 for width of bitset + } + else + { + LSR(physical_addr, effective_addr, 5 + 5); } - // Check whether a JIT cache line needs to be invalidated. - LSR(physical_addr, physical_addr, 5 + 5); // >> 5 for cache line size, >> 5 for width of bitset MOVP2R(EncodeRegTo64(WA), GetBlockCache()->GetBlockBitSet()); LDR(physical_addr, EncodeRegTo64(WA), ArithOption(EncodeRegTo64(physical_addr), true)); From e73bcbd8180361bb1bcf388bf34276ea7393125d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Fri, 12 Jun 2026 18:30:21 +0200 Subject: [PATCH 084/270] MenuBar: Add missing Alt shortcuts in Tools A few actions were missing from shortcuts in the Tools menu. This allows using e.g. `ALT`+(`T`>`E`>`S`) to open the Skylanders Manager, or `ALT`+(`C`>`C`>`1`) to toggle the first Wii Remote's connection. --- Source/Core/DolphinQt/MenuBar.cpp | 58 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index f5d87e0b775f..e37db15d5caa 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -282,9 +282,9 @@ void MenuBar::AddToolsMenu() tools_menu->addAction(tr("&Cheats Manager"), this, [this] { emit ShowCheatsManager(); }); - tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer); + tools_menu->addAction(tr("&FIFO Player"), this, &MenuBar::ShowFIFOPlayer); - auto* usb_device_menu = new QMenu(tr("Emulated USB Devices"), tools_menu); + auto* usb_device_menu = new QMenu(tr("&Emulated USB Devices"), tools_menu); usb_device_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); usb_device_menu->addAction(tr("&Infinity Base"), this, &MenuBar::ShowInfinityBase); usb_device_menu->addAction(tr("&Wii Speak"), this, &MenuBar::ShowWiiSpeakWindow); @@ -300,7 +300,7 @@ void MenuBar::AddToolsMenu() #ifdef USE_RETRO_ACHIEVEMENTS m_achievements_action = - tools_menu->addAction(tr("Achievements"), this, [this] { emit ShowAchievementsWindow(); }); + tools_menu->addAction(tr("&Achievements"), this, [this] { emit ShowAchievementsWindow(); }); #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION m_achievements_dev_menu = tools_menu->addMenu(tr("RetroAchievements Development")); m_raintegration_event_hook = AchievementManager::GetInstance().dev_menu_update_event.Register( @@ -310,61 +310,61 @@ void MenuBar::AddToolsMenu() tools_menu->addSeparator(); #endif // USE_RETRO_ACHIEVEMENTS - QMenu* gc_ipl = tools_menu->addMenu(tr("Load GameCube Main Menu")); + QMenu* gc_ipl = tools_menu->addMenu(tr("Load &GameCube Main Menu")); - m_ntscj_ipl = gc_ipl->addAction(tr("NTSC-J"), this, + m_ntscj_ipl = gc_ipl->addAction(tr("NTSC-&J"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::NTSC_J); }); - m_ntscu_ipl = gc_ipl->addAction(tr("NTSC-U"), this, + m_ntscu_ipl = gc_ipl->addAction(tr("NTSC-&U"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::NTSC_U); }); m_pal_ipl = - gc_ipl->addAction(tr("PAL"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::PAL); }); + gc_ipl->addAction(tr("&PAL"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::PAL); }); - m_dev_ipl = gc_ipl->addAction(tr("Triforce"), this, + m_dev_ipl = gc_ipl->addAction(tr("&Triforce"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::Unknown); }); - tools_menu->addAction(tr("Memory Card Manager"), this, [this] { emit ShowMemcardManager(); }); + tools_menu->addAction(tr("&Memory Card Manager"), this, [this] { emit ShowMemcardManager(); }); tools_menu->addSeparator(); // Label will be set by a NANDRefresh later m_boot_sysmenu = tools_menu->addAction(QString{}, this, [this] { emit BootWiiSystemMenu(); }); - m_wad_install_action = tools_menu->addAction(tr("Install WAD..."), this, &MenuBar::InstallWAD); - m_manage_nand_menu = tools_menu->addMenu(tr("Manage NAND")); - m_import_backup = m_manage_nand_menu->addAction(tr("Import BootMii NAND Backup..."), this, + m_wad_install_action = tools_menu->addAction(tr("Install &WAD..."), this, &MenuBar::InstallWAD); + m_manage_nand_menu = tools_menu->addMenu(tr("Manage &NAND")); + m_import_backup = m_manage_nand_menu->addAction(tr("Import &BootMii NAND Backup..."), this, [this] { emit ImportNANDBackup(); }); - m_check_nand = m_manage_nand_menu->addAction(tr("Check NAND..."), this, &MenuBar::CheckNAND); - m_extract_certificates = m_manage_nand_menu->addAction(tr("Extract Certificates from NAND"), this, - &MenuBar::NANDExtractCertificates); + m_check_nand = m_manage_nand_menu->addAction(tr("Check &NAND..."), this, &MenuBar::CheckNAND); + m_extract_certificates = m_manage_nand_menu->addAction(tr("Extract &Certificates from NAND"), + this, &MenuBar::NANDExtractCertificates); m_boot_sysmenu->setEnabled(false); connect(&Settings::Instance(), &Settings::NANDRefresh, this, [this] { UpdateToolsMenu(Core::State::Uninitialized); }); - m_perform_online_update_menu = tools_menu->addMenu(tr("Perform Online System Update")); + m_perform_online_update_menu = tools_menu->addMenu(tr("Perform Online System &Update")); m_perform_online_update_for_current_region = m_perform_online_update_menu->addAction( - tr("Current Region"), this, [this] { emit PerformOnlineUpdate(""); }); + tr("&Current Region"), this, [this] { emit PerformOnlineUpdate(""); }); m_perform_online_update_menu->addSeparator(); - m_perform_online_update_menu->addAction(tr("Europe"), this, + m_perform_online_update_menu->addAction(tr("&Europe"), this, [this] { emit PerformOnlineUpdate("EUR"); }); - m_perform_online_update_menu->addAction(tr("Japan"), this, + m_perform_online_update_menu->addAction(tr("&Japan"), this, [this] { emit PerformOnlineUpdate("JPN"); }); - m_perform_online_update_menu->addAction(tr("Korea"), this, + m_perform_online_update_menu->addAction(tr("&Korea"), this, [this] { emit PerformOnlineUpdate("KOR"); }); - m_perform_online_update_menu->addAction(tr("United States"), this, + m_perform_online_update_menu->addAction(tr("&United States"), this, [this] { emit PerformOnlineUpdate("USA"); }); tools_menu->addSeparator(); m_import_wii_save = - tools_menu->addAction(tr("Import Wii Save..."), this, &MenuBar::ImportWiiSave); + tools_menu->addAction(tr("Import &Wii Save..."), this, &MenuBar::ImportWiiSave); m_import_wii_saves = - tools_menu->addAction(tr("Import Wii Saves..."), this, &MenuBar::ImportWiiSaves); + tools_menu->addAction(tr("Import &Wii Saves..."), this, &MenuBar::ImportWiiSaves); m_export_wii_saves = - tools_menu->addAction(tr("Export All Wii Saves"), this, &MenuBar::ExportWiiSaves); + tools_menu->addAction(tr("Export All &Wii Saves"), this, &MenuBar::ExportWiiSaves); auto* const connect_wii_remotes_menu{ - new QtUtils::NonAutodismissibleMenu(tr("Connect Wii Remotes"), tools_menu)}; + new QtUtils::NonAutodismissibleMenu(tr("&Connect Wii Remotes"), tools_menu)}; tools_menu->addSeparator(); tools_menu->addMenu(connect_wii_remotes_menu); @@ -372,13 +372,13 @@ void MenuBar::AddToolsMenu() for (int i = 0; i < 4; i++) { m_wii_remotes[i] = connect_wii_remotes_menu->addAction( - tr("Connect Wii Remote %1").arg(i + 1), this, [this, i] { emit ConnectWiiRemote(i); }); + tr("Connect Wii Remote &%1").arg(i + 1), this, [this, i] { emit ConnectWiiRemote(i); }); m_wii_remotes[i]->setCheckable(true); } connect_wii_remotes_menu->addSeparator(); - m_wii_remotes[4] = connect_wii_remotes_menu->addAction(tr("Connect Balance Board"), this, + m_wii_remotes[4] = connect_wii_remotes_menu->addAction(tr("Connect &Balance Board"), this, [this] { emit ConnectWiiRemote(4); }); m_wii_remotes[4]->setCheckable(true); } @@ -1126,8 +1126,8 @@ void MenuBar::UpdateToolsMenu(const Core::State state) DiscIO::GetSysMenuVersionString(tmd.GetTitleVersion(), tmd.IsvWii())) : QString{}; - const QString sysmenu_text = (tmd.IsValid() && tmd.IsvWii()) ? tr("Load vWii System Menu %1") : - tr("Load Wii System Menu %1"); + const QString sysmenu_text = (tmd.IsValid() && tmd.IsvWii()) ? tr("Load vWii &System Menu %1") : + tr("Load Wii &System Menu %1"); m_boot_sysmenu->setText(sysmenu_text.arg(sysmenu_version)); From bb41867ffe49309bb8618e15c3a2c26091b1967a Mon Sep 17 00:00:00 2001 From: "Mateus B. Cassiano" Date: Sat, 13 Jun 2026 02:39:46 -0400 Subject: [PATCH 085/270] Externals: ensure glslang respects the selected toolset from Visual Studio --- Externals/glslang/glslang.vcxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Externals/glslang/glslang.vcxproj b/Externals/glslang/glslang.vcxproj index 6ead065932b9..867296c31e14 100644 --- a/Externals/glslang/glslang.vcxproj +++ b/Externals/glslang/glslang.vcxproj @@ -32,10 +32,10 @@ amd64 arm64 call vsdevcmd.bat -arch=$(DevCmdArch) - cmake -G $(CmakeGenerator) -A $(Platform) -DCMAKE_BUILD_TYPE="Release" -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DGLSLANG_TESTS=OFF -DENABLE_GLSLANG_BINARIES=OFF -DBUILD_EXTERNAL=OFF -DENABLE_SPVREMAPPER=OFF -DENABLE_HLSL=OFF -DENABLE_OPT=OFF -DENABLE_EXCEPTIONS=OFF -S glslang -B "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" + cmake -G $(CmakeGenerator) -A $(Platform) -T $(PlatformToolset) -DCMAKE_BUILD_TYPE="Release" -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DGLSLANG_TESTS=OFF -DENABLE_GLSLANG_BINARIES=OFF -DBUILD_EXTERNAL=OFF -DENABLE_SPVREMAPPER=OFF -DENABLE_HLSL=OFF -DENABLE_OPT=OFF -DENABLE_EXCEPTIONS=OFF -S glslang -B "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" call vsdevcmd.bat -arch=$(DevCmdArch) - cmake -G $(CmakeGenerator) -A $(Platform) -DCMAKE_BUILD_TYPE="Debug" -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug -DGLSLANG_TESTS=OFF -DENABLE_GLSLANG_BINARIES=OFF -DBUILD_EXTERNAL=OFF -DENABLE_SPVREMAPPER=OFF -DENABLE_HLSL=OFF -DENABLE_OPT=OFF -DENABLE_EXCEPTIONS=OFF -S glslang -B "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" + cmake -G $(CmakeGenerator) -A $(Platform) -T $(PlatformToolset) -DCMAKE_BUILD_TYPE="Debug" -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug -DGLSLANG_TESTS=OFF -DENABLE_GLSLANG_BINARIES=OFF -DBUILD_EXTERNAL=OFF -DENABLE_SPVREMAPPER=OFF -DENABLE_HLSL=OFF -DENABLE_OPT=OFF -DENABLE_EXCEPTIONS=OFF -S glslang -B "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" echo Copying.. From 9463c9717f1dfbe9225c668858b1ae8789a1993d Mon Sep 17 00:00:00 2001 From: Tillmann Karras Date: Sat, 13 Jun 2026 18:48:39 +0100 Subject: [PATCH 086/270] GameSettings: fix pony not showing up in Pony Friends 2 EFB-to-RAM needs to be enabled at the start of grooming. --- Data/Sys/GameSettings/R2R.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Data/Sys/GameSettings/R2R.ini b/Data/Sys/GameSettings/R2R.ini index 0ce092f26da6..766b75d72cac 100644 --- a/Data/Sys/GameSettings/R2R.ini +++ b/Data/Sys/GameSettings/R2R.ini @@ -2,3 +2,7 @@ [Video_Settings] SuggestedAspectRatio = 2 + +[Video_Hacks] +# Required to show pony for grooming. +EFBToTextureEnable = False From 2204a789e4238b5ee0802866b4de7be963f43d19 Mon Sep 17 00:00:00 2001 From: Martino Fontana Date: Sun, 14 Jun 2026 00:10:09 +0200 Subject: [PATCH 087/270] PPCAnalyst: In `IsBusyWaitLoop`, ignore `nop` Found out when attempting to make a speed hack to help Dolphin detect an idle loop. The inserted `nop` was preventing it from considering it an idle loop. --- Source/Core/Core/PowerPC/PPCAnalyst.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Core/Core/PowerPC/PPCAnalyst.cpp b/Source/Core/Core/PowerPC/PPCAnalyst.cpp index 40b974b0d8f1..bb978213eed5 100644 --- a/Source/Core/Core/PowerPC/PPCAnalyst.cpp +++ b/Source/Core/Core/PowerPC/PPCAnalyst.cpp @@ -757,6 +757,12 @@ bool PPCAnalyzer::IsBusyWaitLoop(CodeBlock* block, CodeOp* code, size_t instruct if (code[i].branchTo == block->m_address && i == instructions) return true; } + // A `nop` is actually a `ori r0, r0, 0`, which would violate the rules (unless `r0` was written + // earlier). + else if (code[i].inst.hex == 0x60000000) + { + continue; + } else if (code[i].opinfo->type != OpType::Integer && code[i].opinfo->type != OpType::Load) { // In the future, some subsets of other instruction types might get From ec0c4b72ff3f20d7d4db3d8561cdc52e2a356af6 Mon Sep 17 00:00:00 2001 From: Sintendo <3380580+Sintendo@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:22:02 +0200 Subject: [PATCH 088/270] Remove unused headers --- Source/Core/Common/CMakeLists.txt | 2 - Source/Core/Common/Semaphore.h | 72 ------------ Source/Core/Common/StringLiteral.h | 20 ---- Source/Core/Core/CMakeLists.txt | 1 - Source/Core/Core/Debugger/GCELF.h | 110 ------------------- Source/Core/DolphinLib.props | 4 - Source/Core/VideoBackends/OGL/CMakeLists.txt | 1 - Source/Core/VideoBackends/OGL/GPUTimer.h | 104 ------------------ 8 files changed, 314 deletions(-) delete mode 100644 Source/Core/Common/Semaphore.h delete mode 100644 Source/Core/Common/StringLiteral.h delete mode 100644 Source/Core/Core/Debugger/GCELF.h delete mode 100644 Source/Core/VideoBackends/OGL/GPUTimer.h diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 894fd3812874..c7459b0853f1 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -132,7 +132,6 @@ add_library(common ScopeGuard.h SDCardUtil.cpp SDCardUtil.h - Semaphore.h SettingsHandler.cpp SettingsHandler.h SFMLHelper.cpp @@ -142,7 +141,6 @@ add_library(common SocketContext.h SpanUtils.h SPSCQueue.h - StringLiteral.h StringUtil.cpp StringUtil.h SymbolDB.cpp diff --git a/Source/Core/Common/Semaphore.h b/Source/Core/Common/Semaphore.h deleted file mode 100644 index cc87550148bb..000000000000 --- a/Source/Core/Common/Semaphore.h +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2016 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#ifdef _WIN32 - -#include - -namespace Common -{ -class Semaphore -{ -public: - Semaphore(int initial_count, int maximum_count) - { - m_handle = CreateSemaphoreA(nullptr, initial_count, maximum_count, nullptr); - } - - ~Semaphore() { CloseHandle(m_handle); } - void Wait() { WaitForSingleObject(m_handle, INFINITE); } - void Post() { ReleaseSemaphore(m_handle, 1, nullptr); } - -private: - HANDLE m_handle; -}; -} // namespace Common - -#elif defined(__APPLE__) - -#include - -namespace Common -{ -class Semaphore -{ -public: - Semaphore(int initial_count, int maximum_count) - { - m_handle = dispatch_semaphore_create(0); - for (int i = 0; i < initial_count; i++) - dispatch_semaphore_signal(m_handle); - } - ~Semaphore() { dispatch_release(m_handle); } - void Wait() { dispatch_semaphore_wait(m_handle, DISPATCH_TIME_FOREVER); } - void Post() { dispatch_semaphore_signal(m_handle); } - -private: - dispatch_semaphore_t m_handle; -}; -} // namespace Common - -#else - -#include - -namespace Common -{ -class Semaphore -{ -public: - Semaphore(int initial_count, int maximum_count) { sem_init(&m_handle, 0, initial_count); } - ~Semaphore() { sem_destroy(&m_handle); } - void Wait() { sem_wait(&m_handle); } - void Post() { sem_post(&m_handle); } - -private: - sem_t m_handle; -}; -} // namespace Common - -#endif // _WIN32 diff --git a/Source/Core/Common/StringLiteral.h b/Source/Core/Common/StringLiteral.h deleted file mode 100644 index 6da9afd24d5c..000000000000 --- a/Source/Core/Common/StringLiteral.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include - -namespace Common -{ -// A useful template for passing string literals as arguments to templates -// from: https://ctrpeach.io/posts/cpp20-string-literal-template-parameters/ -template -struct StringLiteral -{ - consteval StringLiteral(const char (&str)[N]) { std::copy_n(str, N, value); } - - char value[N]; -}; - -} // namespace Common diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index e411239d1774..996b63e22bd1 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -71,7 +71,6 @@ add_library(core Debugger/Debugger_SymbolMap.h Debugger/Dump.cpp Debugger/Dump.h - Debugger/GCELF.h Debugger/OSThread.cpp Debugger/OSThread.h Debugger/PPCDebugInterface.cpp diff --git a/Source/Core/Core/Debugger/GCELF.h b/Source/Core/Core/Debugger/GCELF.h deleted file mode 100644 index 18faa4813e2e..000000000000 --- a/Source/Core/Core/Debugger/GCELF.h +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2008 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -// ELF File Types -#define ET_NONE 0 // No file type -#define ET_REL 1 // Relocatable file -#define ET_EXEC 2 // Executable file -#define ET_DYN 3 // Shared object file -#define ET_CORE 4 // Core file -#define ET_LOPROC 0xFF00 // Processor specific -#define ET_HIPROC 0xFFFF // Processor specific - -// ELF Machine Types -#define EM_NONE 0 // No machine -#define EM_M32 1 // AT&T WE 32100 -#define EM_SPARC 2 // SPARC -#define EM_386 3 // Intel Architecture -#define EM_68K 4 // Motorola 68000 -#define EM_88K 5 // Motorola 88000 -#define EM_860 6 // Intel 80860 -#define EM_MIPS 7 // MIPS RS3000 Big-Endian -#define EM_MIPS_RS4_BE 8 // MIPS RS4000 Big-Endian -#define EM_ARM 40 // ARM/Thumb Architecture - -// ELF Version Types -#define EV_NONE 0 // Invalid version -#define EV_CURRENT 1 // Current version - -// ELF Section Header Types -#define SHT_NULL 0 -#define SHT_PROGBITS 1 -#define SHT_SYMTAB 2 -#define SHT_STRTAB 3 -#define SHT_RELA 4 -#define SHT_HASH 5 -#define SHT_DYNAMIC 6 -#define SHT_NOTE 7 -#define SHT_NOBITS 8 -#define SHT_REL 9 -#define SHT_SHLIB 10 -#define SHT_DYNSYM 11 - -struct ELF_Header -{ - unsigned char ID[4]; - unsigned char clazz; - unsigned char data; - unsigned char version; - unsigned char pad[9]; - unsigned short e_type; // ELF file type - unsigned short e_machine; // ELF target machine - unsigned int e_version; // ELF file version number - unsigned int e_entry; - unsigned int e_phoff; - unsigned int e_shoff; - unsigned int e_flags; - unsigned short e_ehsize; - unsigned short e_phentsize; - unsigned short e_phnum; - unsigned short e_shentsize; - unsigned short e_shnum; - unsigned short e_shtrndx; -}; - -struct Program_Header -{ - unsigned int type; - unsigned int offset; - unsigned int vaddr; - unsigned int paddr; - unsigned int filesz; - unsigned int memsz; - unsigned int flags; - unsigned int align; -}; - -struct Section_Header -{ - unsigned int name; - unsigned int type; - unsigned int flags; - unsigned int addr; - unsigned int offset; - unsigned int size; - unsigned int link; - unsigned int info; - unsigned int addralign; - unsigned int entsize; -}; - -struct Symbol_Header -{ - unsigned int name; - unsigned int value; - unsigned int size; - unsigned char info; - unsigned char other; - unsigned short shndx; -}; - -struct Rela_Header -{ - unsigned int offset; - unsigned int info; - signed int addend; -}; - -const char ELFID[4] = {0x7F, 'E', 'L', 'F'}; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 9959caf60cf8..f1dec7b3b584 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -159,14 +159,12 @@ - - @@ -222,7 +220,6 @@ - @@ -633,7 +630,6 @@ - diff --git a/Source/Core/VideoBackends/OGL/CMakeLists.txt b/Source/Core/VideoBackends/OGL/CMakeLists.txt index 30c4f98ba23b..d48624b7e7b7 100644 --- a/Source/Core/VideoBackends/OGL/CMakeLists.txt +++ b/Source/Core/VideoBackends/OGL/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(videoogl - GPUTimer.h OGLBoundingBox.cpp OGLBoundingBox.h OGLConfig.cpp diff --git a/Source/Core/VideoBackends/OGL/GPUTimer.h b/Source/Core/VideoBackends/OGL/GPUTimer.h deleted file mode 100644 index 2beefa839e54..000000000000 --- a/Source/Core/VideoBackends/OGL/GPUTimer.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2016 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "Common/GL/GLExtensions/GLExtensions.h" - -#ifndef GL_TIME_ELAPSED -#define GL_TIME_ELAPSED 0x88BF -#endif - -namespace OGL -{ -/* - * This class can be used to measure the time it takes for the GPU to perform a draw call - * or compute dispatch. To use: - * - * - Create an instance of GPUTimer before issuing the draw call. - * (this can be before or after any binding that needs to be done) - * - * - (optionally) call Begin(). This is not needed for a single draw call. - * - * - Issue the draw call or compute dispatch as normal. - * - * - (optionally) call End(). This is not necessary for a single draw call. - * - * - Call GetTime{Seconds,Milliseconds,Nanoseconds} to determine how long the operation - * took to execute on the GPU. - * - * NOTE: When the timer is read back, this will force a GL flush, so the more often a timer is used, - * the larger of a performance impact it will have. Only one timer can be active at any time, due to - * using GL_TIME_ELAPSED. This is not enforced by the class, however. - * - */ -class GPUTimer final -{ -public: - GPUTimer() - { - glGenQueries(1, &m_query_id); - Begin(); - } - - ~GPUTimer() - { - End(); - glDeleteQueries(1, &m_query_id); - } - - void Begin() - { - if (m_started) - glEndQuery(GL_TIME_ELAPSED); - - glBeginQuery(GL_TIME_ELAPSED, m_query_id); - m_started = true; - } - - void End() - { - if (!m_started) - return; - - glEndQuery(GL_TIME_ELAPSED); - m_started = false; - } - - double GetTimeSeconds() - { - GetResult(); - return static_cast(m_result) / 1000000000.0; - } - - double GetTimeMilliseconds() - { - GetResult(); - return static_cast(m_result) / 1000000.0; - } - - u32 GetTimeNanoseconds() - { - GetResult(); - return m_result; - } - -private: - void GetResult() - { - if (m_has_result) - return; - - if (m_started) - End(); - - glGetQueryObjectuiv(m_query_id, GL_QUERY_RESULT, &m_result); - m_has_result = true; - } - - GLuint m_query_id; - GLuint m_result = 0; - bool m_started = false; - bool m_has_result = false; -}; -} // namespace OGL From 8200d9319987feb100c8ba121c032eb255b85510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Tue, 23 Jun 2026 22:45:26 +0200 Subject: [PATCH 089/270] CMakeLists: Remove redundant clang version check The minimum clang version is currently 15 --- CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f07566c044d5..dacb51c3820f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -402,12 +402,6 @@ if(CMAKE_SYSTEM_NAME MATCHES "FreeBSD") set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH};/usr/local") set(CMAKE_REQUIRED_INCLUDES "/usr/local/include") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib") - - if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14.0) - # Workaround: the llvm libc++ and versions of clang earlier than 14 have a bug with consteval - # so we define FMT_CONSTEVAL to blank to just disable consteval in fmt - add_definitions(-DFMT_CONSTEVAL=) - endif() endif() # Dolphin requires threads. From 82a317124ade2392ad04dd47582c14e839a4c998 Mon Sep 17 00:00:00 2001 From: Link Mauve Date: Wed, 6 May 2026 22:24:49 +0200 Subject: [PATCH 090/270] HW/ProcessorInterface: Add support for all known registers These changes originate from testing the behaviour of the PI registers in Linux on my Wii. I started by testing the masks, writing 0xFFFF_FFFF to the registers on the Wii, and then got told about their value on a GameCube, and then figured out most of the registers through a discussion with Extrems. --- Source/Core/Core/HW/ProcessorInterface.cpp | 42 +++++++++++++++++++--- Source/Core/Core/HW/ProcessorInterface.h | 25 ++++++++++++- Source/Core/Core/State.cpp | 2 +- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/Source/Core/Core/HW/ProcessorInterface.cpp b/Source/Core/Core/HW/ProcessorInterface.cpp index 7b999ac0de5c..10cca6c1c5a4 100644 --- a/Source/Core/Core/HW/ProcessorInterface.cpp +++ b/Source/Core/Core/HW/ProcessorInterface.cpp @@ -41,20 +41,27 @@ void ProcessorInterfaceManager::DoState(PointerWrap& p) p.Do(m_fifo_cpu_base); p.Do(m_fifo_cpu_end); p.Do(m_fifo_cpu_write_pointer); + p.Do(m_error_cause); + p.Do(m_error_address); p.Do(m_reset_code); + p.Do(m_unknown); + p.Do(m_flipper_bus_strength); } void ProcessorInterfaceManager::Init() { m_interrupt_mask = 0; - m_interrupt_cause = 0; + m_interrupt_cause = INT_CAUSE_RST_BUTTON | INT_CAUSE_VI; m_fifo_cpu_base = 0; m_fifo_cpu_end = 0; m_fifo_cpu_write_pointer = 0; + m_error_cause = 0; + m_error_address = 0; m_reset_code = 0; // Cold reset - m_interrupt_cause = INT_CAUSE_RST_BUTTON | INT_CAUSE_VI; + m_unknown = 0x000001FF; + m_flipper_bus_strength = 0x02492492; auto& core_timing = m_system.GetCoreTiming(); m_event_type_toggle_reset_button = @@ -82,13 +89,25 @@ void ProcessorInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base) })); mmio->Register(base | PI_FIFO_BASE, MMIO::DirectRead(&m_fifo_cpu_base), - MMIO::DirectWrite(&m_fifo_cpu_base, 0xFFFFFFE0)); + MMIO::ComplexWrite([](Core::System& system, u32, u32 val) { + u32 mask = CommandProcessor::GetPhysicalAddressMask(system.IsWii()) & 0xFFFFFFE0; + auto& processor_interface = system.GetProcessorInterface(); + processor_interface.m_fifo_cpu_base = val & mask; + })); mmio->Register(base | PI_FIFO_END, MMIO::DirectRead(&m_fifo_cpu_end), - MMIO::DirectWrite(&m_fifo_cpu_end, 0xFFFFFFE0)); + MMIO::ComplexWrite([](Core::System& system, u32, u32 val) { + u32 mask = CommandProcessor::GetPhysicalAddressMask(system.IsWii()) & 0xFFFFFFE0; + auto& processor_interface = system.GetProcessorInterface(); + processor_interface.m_fifo_cpu_end = val & mask; + })); mmio->Register(base | PI_FIFO_WPTR, MMIO::DirectRead(&m_fifo_cpu_write_pointer), - MMIO::DirectWrite(&m_fifo_cpu_write_pointer, 0xFFFFFFE0)); + MMIO::ComplexWrite([](Core::System& system, u32, u32 val) { + u32 mask = CommandProcessor::GetPhysicalAddressMask(system.IsWii()) & 0xFFFFFFE0; + auto& processor_interface = system.GetProcessorInterface(); + processor_interface.m_fifo_cpu_write_pointer = val & mask; + })); mmio->Register(base | PI_FIFO_RESET, MMIO::InvalidRead(), MMIO::ComplexWrite([](Core::System& system, u32, u32 val) { @@ -116,6 +135,13 @@ void ProcessorInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base) } })); + // TODO: Use the ErrorCause enum instead. + mmio->Register(base | PI_ERROR_CAUSE, MMIO::DirectRead(&m_error_cause), + MMIO::DirectWrite(&m_error_cause, 0x00000007)); + + mmio->Register(base | PI_ERROR_ADDRESS, MMIO::DirectRead(&m_error_address), + MMIO::InvalidWrite()); + mmio->Register(base | PI_RESET_CODE, MMIO::ComplexRead([](Core::System& system, u32) { auto& processor_interface = system.GetProcessorInterface(); DEBUG_LOG_FMT(PROCESSORINTERFACE, "Read PI_RESET_CODE: {:08x}", @@ -133,9 +159,15 @@ void ProcessorInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base) } })); + mmio->Register(base | PI_UNKNOWN, MMIO::DirectRead(&m_unknown), + MMIO::DirectWrite(&m_unknown, 0x000003FF)); + mmio->Register(base | PI_FLIPPER_REV, MMIO::Constant(FLIPPER_REV_C), MMIO::InvalidWrite()); + mmio->Register(base | PI_FLIPPER_BUS_STRENGTH, MMIO::DirectRead(&m_flipper_bus_strength), + MMIO::DirectWrite(&m_flipper_bus_strength, 0x07FFFFFF)); + // 16 bit reads are based on 32 bit reads. for (u32 i = 0; i < 0x1000; i += 4) { diff --git a/Source/Core/Core/HW/ProcessorInterface.h b/Source/Core/Core/HW/ProcessorInterface.h index 6387634843d7..493414760919 100644 --- a/Source/Core/Core/HW/ProcessorInterface.h +++ b/Source/Core/Core/HW/ProcessorInterface.h @@ -55,9 +55,27 @@ enum PI_FIFO_END = 0x10, PI_FIFO_WPTR = 0x14, PI_FIFO_RESET = 0x18, // Used by GXAbortFrame + PI_ERROR_CAUSE = 0x1C, + PI_ERROR_ADDRESS = 0x20, PI_RESET_CODE = 0x24, + PI_UNKNOWN = 0x28, PI_FLIPPER_REV = 0x2C, - PI_FLIPPER_UNK = 0x30 // BS1 writes 0x0245248A to it - prolly some bootstrap thing + PI_FLIPPER_BUS_STRENGTH = 0x30 // BS1 writes 0x0245248A to it - controls the strength of the + // signal on the bus. 0 means the bus is dead and Flipper will + // not respond any longer, increasing it from the default value + // can reduce the noise in the Game Boy Player. +}; + +enum ErrorCause : u32 +{ + NoError = 0, + MisalignedAddress = 1, + IncorrectTransferType = 2, + UnsupportedTransferSize = 3, + AddressOutOfRange = 4, + WriteToROM = 5, + ReadFromGXFIFO = 6, + Reserved = 7, }; class ProcessorInterfaceManager @@ -92,6 +110,11 @@ class ProcessorInterfaceManager u32 m_fifo_cpu_end = 0; u32 m_fifo_cpu_write_pointer = 0; + u32 m_error_cause = 0; + u32 m_error_address = 0; + u32 m_unknown = 0x000001FF; + u32 m_flipper_bus_strength = 0x02492492; + private: // Let the PPC know that an external exception is set/cleared void UpdateException(); diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 040c60f6ab3e..345e1537e539 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -96,7 +96,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP s_compress_and_dump_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 191; // Last changed in PR 14668 +constexpr u32 STATE_VERSION = 192; // Last changed in PR 14646 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217 From e87225beb25440c8b6f3b82019028681cf248160 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 28 May 2026 00:40:12 +0200 Subject: [PATCH 091/270] Update bundled Windows Qt to 6.8.3 --- Externals/Qt | 2 +- Source/Core/DolphinQt/CMakeLists.txt | 4 ++-- Source/VSProps/QtCompile.props | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Externals/Qt b/Externals/Qt index 495517af2b92..306289108d29 160000 --- a/Externals/Qt +++ b/Externals/Qt @@ -1 +1 @@ -Subproject commit 495517af2b922c10c24f543e0fd6ea3ddf774e50 +Subproject commit 306289108d29a4b31a5460f7b55d28fde7b96d1b diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 02700d1c1879..f4ba46d2f9ef 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -1,8 +1,8 @@ if (MSVC) if(_M_ARM_64) - list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/Externals/Qt/Qt6.5.1/ARM64") + list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/Externals/Qt/Qt6.8.3/ARM64") else() - list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/Externals/Qt/Qt6.5.1/x64") + list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/Externals/Qt/Qt6.8.3/x64") endif() endif() diff --git a/Source/VSProps/QtCompile.props b/Source/VSProps/QtCompile.props index c42c823f88d7..bc3c4fe410a0 100644 --- a/Source/VSProps/QtCompile.props +++ b/Source/VSProps/QtCompile.props @@ -1,7 +1,7 @@ - $(ExternalsDir)Qt\Qt6.5.1\ + $(ExternalsDir)Qt\Qt6.8.3\ $(ExternalsQtDir)$(Platform)\ $(QtTargetDirDefault) $(QTDIR)\ @@ -113,7 +113,7 @@ - + From 64a73e56361cbc65e35d9a3fd03d33f6103a04a4 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 26 Jun 2026 22:10:16 +0200 Subject: [PATCH 092/270] Android: Fix mixup between Vibrator id and index This fixes https://bugs.dolphin-emu.org/issues/14076. The issue report more or less already says it all, but to provide a shorter summary: We were fetching a list of vibrator IDs, but instead of passing the vibrator ID to the vibrator manager, we passed the index of the ID in the list. This happened to work fine on many devices, including all devices that use DolphinVibratorManagerCompat, due to the only Vibrator having both an index and ID of 0. But on some devices, it failed due to the ID of the Vibrator being 1. This fix makes us correctly pass the ID to the vibrator manager. We still use indices in controller INI files, both for compatibility with the controller mappings shipped with Dolphin (which use index 0) and for backwards compatibility with older controller INI files. --- .../ControllerInterface/Android/Android.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp index 0b0609f01082..40f1e2b09669 100644 --- a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp @@ -584,14 +584,14 @@ class AndroidSensorAxis final : public AndroidAxis class AndroidMotor : public Core::Device::Output { public: - AndroidMotor(JNIEnv* env, jobject vibrator, jint id) - : m_vibrator(env->NewGlobalRef(vibrator)), m_id(id) + AndroidMotor(JNIEnv* env, jobject vibrator, jint index) + : m_vibrator(env->NewGlobalRef(vibrator)), m_index(index) { } ~AndroidMotor() { IDCache::GetEnvForThread()->DeleteGlobalRef(m_vibrator); } - std::string GetName() const override { return "Motor " + std::to_string(m_id); } + std::string GetName() const override { return "Motor " + std::to_string(m_index); } void SetState(ControlState state) override { @@ -606,7 +606,7 @@ class AndroidMotor : public Core::Device::Output private: const jobject m_vibrator; - const jint m_id; + const jint m_index; std::atomic m_state = 0; }; @@ -793,8 +793,8 @@ class AndroidDevice final : public Core::Device jint size = env->GetArrayLength(j_vibrator_ids); for (jint i = 0; i < size; ++i) { - jobject vibrator = - env->CallObjectMethod(vibrator_manager, s_dolphin_vibrator_manager_get_vibrator, i); + jobject vibrator = env->CallObjectMethod( + vibrator_manager, s_dolphin_vibrator_manager_get_vibrator, vibrator_ids[i]); AddOutput(new AndroidMotor(env, vibrator, i)); env->DeleteLocalRef(vibrator); } From af6c6ffacff3a0d7b2e1b5d1b28322848ed86c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Sat, 27 Jun 2026 09:52:14 +0200 Subject: [PATCH 093/270] Externals/miniupnpc: Disable warnings --- Externals/miniupnpc/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Externals/miniupnpc/CMakeLists.txt b/Externals/miniupnpc/CMakeLists.txt index 3b931bd7e3fd..33f34fb7a81a 100644 --- a/Externals/miniupnpc/CMakeLists.txt +++ b/Externals/miniupnpc/CMakeLists.txt @@ -5,5 +5,6 @@ set(UPNPC_BUILD_SAMPLE OFF) set(UPNPC_NO_INSTALL ON) add_subdirectory(miniupnp/miniupnpc) +dolphin_disable_warnings(libminiupnpc-static) add_library(Miniupnpc::miniupnpc ALIAS libminiupnpc-static) From 200e26c98acad07e1531b325de0a6015ed90d938 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 25 Apr 2026 14:57:58 +0200 Subject: [PATCH 094/270] Core: Remove SerialInterfaceManager::ChangeDevice Now SerialInterfaceManager::UpdateDevices reads the configured SI devices directly from Config instead. The main reason why I wanted to do this is so that Android can change SI devices while emulation is running. (Android didn't have the code for calling ChangeDevice.) But when implementing the change, I noticed that NetPlay and Movie were using ChangeDevice as a way of overriding the SI devices configured by the user. Replacing this ended up making the change larger than I first anticipated. For Wii Remotes, there was no equivalent to ChangeDevice, so NetPlay and Movie were using Config::SetCurrent to override the Wii Remote source configured by the user. If we can use the config system to override Wii Remote sources, why not do the same for SI devices? This commit makes NetPlay and Movie set SI devices and Wii Remote sources in the NetPlay and Movie config layers, as that is the conceptually appropriate place to do it. As far as I can tell, the old Movie code for overriding SI devices and Wii Remote sources didn't actually work. This new code does. I didn't investigate exactly why it didn't work, but maybe it's because loading a movie happens before emulation actually starts. --- .../features/settings/model/IntSetting.kt | 6 +- .../Core/ConfigLoaders/MovieConfigLoader.cpp | 36 ++++++ .../ConfigLoaders/NetPlayConfigLoader.cpp | 54 ++++++++ Source/Core/Core/HW/SI/SI.cpp | 36 +----- Source/Core/Core/HW/SI/SI.h | 5 - Source/Core/Core/Movie.cpp | 47 +------ Source/Core/Core/Movie.h | 2 +- Source/Core/Core/NetPlayClient.cpp | 120 +++++------------- Source/Core/Core/NetPlayClient.h | 5 - Source/Core/Core/NetPlayProto.h | 33 +++-- .../Config/GamecubeControllersWidget.cpp | 7 - 11 files changed, 149 insertions(+), 202 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt index 1802357e0d2e..84ae167ad704 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt @@ -209,11 +209,7 @@ enum class IntSetting( MAIN_SLOT_A, MAIN_SLOT_B, MAIN_SERIAL_PORT_1, - MAIN_FALLBACK_REGION, - MAIN_SI_DEVICE_0, - MAIN_SI_DEVICE_1, - MAIN_SI_DEVICE_2, - MAIN_SI_DEVICE_3 + MAIN_FALLBACK_REGION ) private val NOT_RUNTIME_EDITABLE: Set = diff --git a/Source/Core/Core/ConfigLoaders/MovieConfigLoader.cpp b/Source/Core/Core/ConfigLoaders/MovieConfigLoader.cpp index b310fb31a44b..6b284719f071 100644 --- a/Source/Core/Core/ConfigLoaders/MovieConfigLoader.cpp +++ b/Source/Core/Core/ConfigLoaders/MovieConfigLoader.cpp @@ -14,6 +14,10 @@ #include "Core/Config/MainSettings.h" #include "Core/Config/SYSCONFSettings.h" #include "Core/Config/SessionSettings.h" +#include "Core/Config/WiimoteSettings.h" +#include "Core/HW/SI/SI.h" +#include "Core/HW/SI/SI_Device.h" +#include "Core/HW/Wiimote.h" #include "Core/Movie.h" namespace PowerPC @@ -50,6 +54,38 @@ static void LoadFromDTM(Config::Layer* config_layer, Movie::DTMHeader* dtm) config_layer->Set(Config::SESSION_USE_FMA, dtm->bUseFMA); config_layer->Set(Config::MAIN_JIT_FOLLOW_BRANCH, dtm->bFollowBranch); + + for (int i = 0; i < SerialInterface::MAX_SI_CHANNELS; ++i) + { + SerialInterface::SIDevices device = SerialInterface::SIDEVICE_NONE; + const auto config_info = Config::GetInfoForSIDevice(i); + if (dtm->GBAControllers & (1 << i)) + { + device = SerialInterface::SIDEVICE_GC_GBA_EMULATED; + } + else if (dtm->controllers & (1 << i)) + { + const SerialInterface::SIDevices si_device = Config::Get(config_info); + if (SerialInterface::SIDevice_IsGCController(si_device)) + { + device = si_device; + } + else + { + device = (dtm->bongos & (1 << i)) != 0 ? SerialInterface::SIDEVICE_GC_TARUKONGA : + SerialInterface::SIDEVICE_GC_CONTROLLER; + } + } + + config_layer->Set(config_info, device); + } + + for (int i = 0; i < MAX_WIIMOTES; ++i) + { + const bool is_using_wiimote = (dtm->controllers & (1 << (i + 4))) != 0; + config_layer->Set(Config::GetInfoForWiimoteSource(i), + is_using_wiimote ? WiimoteSource::Emulated : WiimoteSource::None); + } } void SaveToDTM(Movie::DTMHeader* dtm) diff --git a/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp b/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp index 7fb9eaa2266b..db60fb00b718 100644 --- a/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp +++ b/Source/Core/Core/ConfigLoaders/NetPlayConfigLoader.cpp @@ -16,9 +16,15 @@ #include "Core/Config/MainSettings.h" #include "Core/Config/SYSCONFSettings.h" #include "Core/Config/SessionSettings.h" +#include "Core/Config/WiimoteSettings.h" #include "Core/HW/EXI/EXI.h" +#include "Core/HW/SI/SI.h" +#include "Core/HW/SI/SI_Device.h" +#include "Core/HW/Wiimote.h" #include "Core/NetPlayProto.h" +#include "InputCommon/GCAdapter.h" + namespace ConfigLoaders { class NetPlayConfigLayerLoader final : public Config::ConfigLayerLoader @@ -127,6 +133,54 @@ class NetPlayConfigLayerLoader final : public Config::ConfigLayerLoader layer->Set(Config::GFX_SSAA, false); } + u8 local_pad = 0; + for (int i = 0; i < SerialInterface::MAX_SI_CHANNELS; ++i) + { + const NetPlay::PlayerId player_id = m_settings.pad_map[i]; + const SerialInterface::SIDevices si_device = + Config::Get(Config::GetInfoForSIDevice(local_pad)); + const auto config_info = Config::GetInfoForSIDevice(i); + + if (m_settings.gba_config[i].enabled && player_id > 0) + { + layer->Set(config_info, SerialInterface::SIDEVICE_GC_GBA_EMULATED); + } + else if (player_id == m_settings.local_player_id) + { + // Use local controller types for local controllers if they are compatible + if (SerialInterface::SIDevice_IsGCController(si_device)) + { + layer->Set(config_info, si_device); + + if (si_device == SerialInterface::SIDEVICE_WIIU_ADAPTER) + { + GCAdapter::ResetDeviceType(local_pad); + } + } + else + { + layer->Set(config_info, SerialInterface::SIDEVICE_GC_CONTROLLER); + } + local_pad++; + } + else if (player_id > 0) + { + if (si_device != SerialInterface::SIDEVICE_AM_BASEBOARD) + layer->Set(config_info, SerialInterface::SIDEVICE_GC_CONTROLLER); + } + else + { + layer->Set(config_info, SerialInterface::SIDEVICE_NONE); + } + } + + for (int i = 0; i < MAX_WIIMOTES; ++i) + { + NetPlay::PlayerId player_id = m_settings.wiimote_map[i]; + layer->Set(Config::GetInfoForWiimoteSource(i), + player_id > 0 ? WiimoteSource::Emulated : WiimoteSource::None); + } + if (m_settings.savedata_load) { if (!m_settings.is_hosting) diff --git a/Source/Core/Core/HW/SI/SI.cpp b/Source/Core/Core/HW/SI/SI.cpp index 11e0c6f2e76a..429897c0dbc5 100644 --- a/Source/Core/Core/HW/SI/SI.cpp +++ b/Source/Core/Core/HW/SI/SI.cpp @@ -269,33 +269,7 @@ void SerialInterfaceManager::Init() m_channel[i].in_lo.hex = 0; m_channel[i].has_recent_device_unplug = false; - auto& movie = m_system.GetMovie(); - if (movie.IsMovieActive()) - { - m_desired_device_types[i] = SIDEVICE_NONE; - - if (movie.IsUsingGBA(i)) - { - m_desired_device_types[i] = SIDEVICE_GC_GBA_EMULATED; - } - else if (movie.IsUsingPad(i)) - { - const SIDevices current = Config::Get(Config::GetInfoForSIDevice(i)); - // GC pad-compatible devices can be used for both playing and recording - if (movie.IsUsingBongo(i)) - m_desired_device_types[i] = SIDEVICE_GC_TARUKONGA; - else if (SIDevice_IsGCController(current)) - m_desired_device_types[i] = current; - else - m_desired_device_types[i] = SIDEVICE_GC_CONTROLLER; - } - } - else if (!NetPlay::IsNetPlayRunning()) - { - m_desired_device_types[i] = Config::Get(Config::GetInfoForSIDevice(i)); - } - - AddDevice(m_desired_device_types[i], i); + AddDevice(Config::Get(Config::GetInfoForSIDevice(i)), i); } m_poll.hex = 0; @@ -499,12 +473,6 @@ void SerialInterfaceManager::AddDevice(const SIDevices device, int device_number AddDevice(SIDevice_Create(m_system, device, device_number)); } -void SerialInterfaceManager::ChangeDevice(SIDevices device, int channel) -{ - // Actual device change will happen in UpdateDevices. - m_desired_device_types[channel] = device; -} - void SerialInterfaceManager::ChangeDeviceDeterministic(SIDevices device, int channel) { if (channel < 0 || channel >= MAX_SI_CHANNELS) @@ -540,7 +508,7 @@ void SerialInterfaceManager::UpdateDevices() for (int i = 0; i != MAX_SI_CHANNELS; ++i) { const SIDevices current_type = GetDeviceType(i); - const SIDevices desired_type = m_desired_device_types[i]; + const SIDevices desired_type = Config::Get(Config::GetInfoForSIDevice(i)); if (current_type != desired_type) { diff --git a/Source/Core/Core/HW/SI/SI.h b/Source/Core/Core/HW/SI/SI.h index 5229bd045fc7..daec278831e2 100644 --- a/Source/Core/Core/HW/SI/SI.h +++ b/Source/Core/Core/HW/SI/SI.h @@ -62,8 +62,6 @@ class SerialInterfaceManager void AddDevice(SIDevices device, int device_number); void AddDevice(std::unique_ptr device); - void ChangeDevice(SIDevices device, int channel); - SIDevices GetDeviceType(int channel) const; u32 GetPollXLines(); @@ -233,9 +231,6 @@ class SerialInterfaceManager CoreTiming::EventType* m_event_type_tranfer_pending = nullptr; std::array m_event_types_device{}; - // User-configured device type. possibly overridden by TAS/Netplay - std::array, MAX_SI_CHANNELS> m_desired_device_types{}; - std::array m_channel; USIPoll m_poll; USIComCSR m_com_csr; diff --git a/Source/Core/Core/Movie.cpp b/Source/Core/Core/Movie.cpp index deaa8088b880..c336becd0794 100644 --- a/Source/Core/Core/Movie.cpp +++ b/Source/Core/Core/Movie.cpp @@ -427,38 +427,10 @@ void MovieManager::ChangePads() else controllers[i] = ControllerType::None; } - - if (m_controllers == controllers) - return; - - auto& si = m_system.GetSerialInterface(); - for (int i = 0; i < SerialInterface::MAX_SI_CHANNELS; ++i) - { - SerialInterface::SIDevices device = SerialInterface::SIDEVICE_NONE; - if (IsUsingGBA(i)) - { - device = SerialInterface::SIDEVICE_GC_GBA_EMULATED; - } - else if (IsUsingPad(i)) - { - const SerialInterface::SIDevices si_device = Config::Get(Config::GetInfoForSIDevice(i)); - if (SerialInterface::SIDevice_IsGCController(si_device)) - { - device = si_device; - } - else - { - device = IsUsingBongo(i) ? SerialInterface::SIDEVICE_GC_TARUKONGA : - SerialInterface::SIDEVICE_GC_CONTROLLER; - } - } - - si.ChangeDevice(device, i); - } } // NOTE: Host / Emu Threads -void MovieManager::ChangeWiiPads(bool instantly) +void MovieManager::ChangeWiiPads() { WiimoteEnabledArray wiimotes{}; @@ -466,21 +438,6 @@ void MovieManager::ChangeWiiPads(bool instantly) { wiimotes[i] = Config::Get(Config::GetInfoForWiimoteSource(i)) != WiimoteSource::None; } - - // This is important for Wiimotes, because they can desync easily if they get re-activated - if (instantly && m_wiimotes == wiimotes) - return; - - const auto bt = WiiUtils::GetBluetoothEmuDevice(); - for (int i = 0; i < MAX_WIIMOTES; ++i) - { - const bool is_using_wiimote = IsUsingWiimote(i); - - Config::SetCurrent(Config::GetInfoForWiimoteSource(i), - is_using_wiimote ? WiimoteSource::Emulated : WiimoteSource::None); - if (bt != nullptr) - bt->AccessWiimoteByIndex(i)->Activate(is_using_wiimote); - } } // NOTE: Host Thread @@ -1011,7 +968,7 @@ void MovieManager::LoadInput(const std::string& movie_path) ChangePads(); if (m_system.IsWii()) - ChangeWiiPads(true); + ChangeWiiPads(); u64 totalSavedBytes = t_record.GetSize() - 256; diff --git a/Source/Core/Core/Movie.h b/Source/Core/Core/Movie.h index 02329df03068..4183111f0b80 100644 --- a/Source/Core/Core/Movie.h +++ b/Source/Core/Core/Movie.h @@ -198,7 +198,7 @@ class MovieManager bool IsUsingBongo(int controller) const; bool IsUsingGBA(int controller) const; void ChangePads(); - void ChangeWiiPads(bool instantly = false); + void ChangeWiiPads(); void SetReadOnly(bool bEnabled); diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index 20edd35c0e03..2cd9ae2801af 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -40,7 +40,6 @@ #include "Core/Config/MainSettings.h" #include "Core/Config/NetplaySettings.h" #include "Core/Config/SessionSettings.h" -#include "Core/Config/WiimoteSettings.h" #include "Core/ConfigManager.h" #include "Core/GeckoCode.h" #include "Core/HW/EXI/EXI.h" @@ -627,17 +626,15 @@ void NetPlayClient::OnChunkedDataAbort(sf::Packet& packet) void NetPlayClient::OnPadMapping(sf::Packet& packet) { - for (PlayerId& mapping : m_pad_map) + for (PlayerId& mapping : m_net_settings.pad_map) packet >> mapping; - UpdateDevices(); - m_dialog->Update(); } void NetPlayClient::OnWiimoteMapping(sf::Packet& packet) { - for (PlayerId& mapping : m_wiimote_map) + for (PlayerId& mapping : m_net_settings.wiimote_map) packet >> mapping; m_dialog->Update(); @@ -645,9 +642,9 @@ void NetPlayClient::OnWiimoteMapping(sf::Packet& packet) void NetPlayClient::OnGBAConfig(sf::Packet& packet) { - for (size_t i = 0; i < m_gba_config.size(); ++i) + for (size_t i = 0; i < m_net_settings.gba_config.size(); ++i) { - auto& config = m_gba_config[i]; + auto& config = m_net_settings.gba_config[i]; const auto old_config = config; packet >> config.enabled >> config.has_rom >> config.title; @@ -666,7 +663,6 @@ void NetPlayClient::OnGBAConfig(sf::Packet& packet) } SendGameStatus(); - UpdateDevices(); m_dialog->Update(); } @@ -680,7 +676,8 @@ void NetPlayClient::OnPadData(sf::Packet& packet) GCPadStatus pad; packet >> pad.button; - if (static_cast(map) < m_gba_config.size() && !m_gba_config.at(map).enabled) + if (static_cast(map) < m_net_settings.gba_config.size() && + !m_net_settings.gba_config.at(map).enabled) { packet >> pad.analogA >> pad.analogB >> pad.stickX >> pad.stickY >> pad.substickX >> pad.substickY >> pad.triggerLeft >> pad.triggerRight >> pad.isConnected; @@ -703,7 +700,8 @@ void NetPlayClient::OnPadHostData(sf::Packet& packet) GCPadStatus pad; packet >> pad.button; - if (static_cast(map) < m_gba_config.size() && !m_gba_config.at(map).enabled) + if (static_cast(map) < m_net_settings.gba_config.size() && + !m_net_settings.gba_config.at(map).enabled) { packet >> pad.analogA >> pad.analogB >> pad.stickX >> pad.stickY >> pad.substickX >> pad.substickY >> pad.triggerLeft >> pad.triggerRight >> pad.isConnected; @@ -1703,7 +1701,7 @@ void NetPlayClient::AddPadStateToPacket(const int in_game_pad, const GCPadStatus { packet << static_cast(in_game_pad); packet << pad.button; - if (!m_gba_config[in_game_pad].enabled) + if (!m_net_settings.gba_config[in_game_pad].enabled) { packet << pad.analogA << pad.analogB << pad.stickX << pad.stickY << pad.substickX << pad.substickY << pad.triggerLeft << pad.triggerRight << pad.isConnected; @@ -1773,23 +1771,17 @@ bool NetPlayClient::StartGame(const std::string& path) Movie::WiimoteEnabledArray wiimotes{}; for (unsigned int i = 0; i < 4; ++i) { - if (m_pad_map[i] > 0 && m_gba_config[i].enabled) + if (m_net_settings.pad_map[i] > 0 && m_net_settings.gba_config[i].enabled) controllers[i] = Movie::ControllerType::GBA; - else if (m_pad_map[i] > 0) + else if (m_net_settings.pad_map[i] > 0) controllers[i] = Movie::ControllerType::GC; else controllers[i] = Movie::ControllerType::None; - wiimotes[i] = m_wiimote_map[i] > 0; + wiimotes[i] = m_net_settings.wiimote_map[i] > 0; } movie.BeginRecordingInput(controllers, wiimotes); } - for (unsigned int i = 0; i < 4; ++i) - { - Config::SetCurrent(Config::GetInfoForWiimoteSource(i), - m_wiimote_map[i] > 0 ? WiimoteSource::Emulated : WiimoteSource::None); - } - // boot game auto boot_session_data = std::make_unique(); @@ -1811,12 +1803,12 @@ bool NetPlayClient::StartGame(const std::string& path) if (File::Exists(redirect_path)) File::DeleteDirRecursively(redirect_path); }); + + m_net_settings.local_player_id = m_local_player->pid; boot_session_data->SetNetplaySettings(std::make_unique(m_net_settings)); m_dialog->BootGame(path, std::move(boot_session_data)); - UpdateDevices(); - return true; } @@ -1880,52 +1872,6 @@ bool NetPlayClient::ChangeGame(const std::string&) return true; } -// called from ---NETPLAY--- thread -void NetPlayClient::UpdateDevices() -{ - u8 local_pad = 0; - u8 pad = 0; - - auto& si = Core::System::GetInstance().GetSerialInterface(); - for (auto player_id : m_pad_map) - { - const SerialInterface::SIDevices si_device = Config::Get(Config::GetInfoForSIDevice(local_pad)); - - if (m_gba_config[pad].enabled && player_id > 0) - { - si.ChangeDevice(SerialInterface::SIDEVICE_GC_GBA_EMULATED, pad); - } - else if (player_id == m_local_player->pid) - { - // Use local controller types for local controllers if they are compatible - if (SerialInterface::SIDevice_IsGCController(si_device)) - { - si.ChangeDevice(si_device, pad); - - if (si_device == SerialInterface::SIDEVICE_WIIU_ADAPTER) - { - GCAdapter::ResetDeviceType(local_pad); - } - } - else - { - si.ChangeDevice(SerialInterface::SIDEVICE_GC_CONTROLLER, pad); - } - local_pad++; - } - else if (player_id > 0) - { - if (si_device != SerialInterface::SIDEVICE_AM_BASEBOARD) - si.ChangeDevice(SerialInterface::SIDEVICE_GC_CONTROLLER, pad); - } - else - { - si.ChangeDevice(SerialInterface::SIDEVICE_NONE, pad); - } - pad++; - } -} - // called from ---NETPLAY--- thread void NetPlayClient::ClearBuffers() { @@ -2182,7 +2128,7 @@ bool NetPlayClient::PollLocalPad(const int local_pad, sf::Packet& packet) bool data_added = false; GCPadStatus pad_status; - if (m_gba_config[ingame_pad].enabled) + if (m_net_settings.gba_config[ingame_pad].enabled) { pad_status = Pad::GetGBAStatus(local_pad); } @@ -2273,9 +2219,9 @@ void NetPlayClient::SendPadHostPoll(const PadIndex pad_num) if (pad_num < 0) { - for (size_t i = 0; i < m_pad_map.size(); i++) + for (size_t i = 0; i < m_net_settings.pad_map.size(); i++) { - if (m_pad_map[i] <= 0) + if (m_net_settings.pad_map[i] <= 0) continue; while (!m_first_pad_status_received[i]) @@ -2287,9 +2233,9 @@ void NetPlayClient::SendPadHostPoll(const PadIndex pad_num) } } - for (size_t i = 0; i < m_pad_map.size(); i++) + for (size_t i = 0; i < m_net_settings.pad_map.size(); i++) { - if (m_pad_map[i] == 0 || m_pad_buffer[i].Size() > 0) + if (m_net_settings.pad_map[i] == 0 || m_pad_buffer[i].Size() > 0) continue; const GCPadStatus& pad_status = m_last_pad_status[i]; @@ -2297,7 +2243,7 @@ void NetPlayClient::SendPadHostPoll(const PadIndex pad_num) AddPadStateToPacket(static_cast(i), pad_status, packet); } } - else if (m_pad_map[pad_num] != 0) + else if (m_net_settings.pad_map[pad_num] != 0) { while (!m_first_pad_status_received[pad_num]) { @@ -2404,18 +2350,18 @@ bool NetPlayClient::LocalPlayerHasControllerMapped() const bool NetPlayClient::IsFirstInGamePad(int ingame_pad) const { - return std::none_of(m_pad_map.begin(), m_pad_map.begin() + ingame_pad, + return std::none_of(m_net_settings.pad_map.begin(), m_net_settings.pad_map.begin() + ingame_pad, [](auto mapping) { return mapping > 0; }); } int NetPlayClient::NumLocalPads() const { - return std::ranges::count(m_pad_map, m_local_player->pid); + return std::ranges::count(m_net_settings.pad_map, m_local_player->pid); } int NetPlayClient::NumLocalWiimotes() const { - return std::ranges::count(m_wiimote_map, m_local_player->pid); + return std::ranges::count(m_net_settings.wiimote_map, m_local_player->pid); } static int InGameToLocal(int ingame_pad, const PadMappingArray& pad_map, PlayerId local_player_pid) @@ -2457,30 +2403,30 @@ static int LocalToInGame(int local_pad, const PadMappingArray& pad_map, PlayerId int NetPlayClient::InGamePadToLocalPad(int ingame_pad) const { - return InGameToLocal(ingame_pad, m_pad_map, m_local_player->pid); + return InGameToLocal(ingame_pad, m_net_settings.pad_map, m_local_player->pid); } int NetPlayClient::LocalPadToInGamePad(int local_pad) const { - return LocalToInGame(local_pad, m_pad_map, m_local_player->pid); + return LocalToInGame(local_pad, m_net_settings.pad_map, m_local_player->pid); } int NetPlayClient::InGameWiimoteToLocalWiimote(int ingame_wiimote) const { - return InGameToLocal(ingame_wiimote, m_wiimote_map, m_local_player->pid); + return InGameToLocal(ingame_wiimote, m_net_settings.wiimote_map, m_local_player->pid); } int NetPlayClient::LocalWiimoteToInGameWiimote(int local_wiimote) const { - return LocalToInGame(local_wiimote, m_wiimote_map, m_local_player->pid); + return LocalToInGame(local_wiimote, m_net_settings.wiimote_map, m_local_player->pid); } bool NetPlayClient::PlayerHasControllerMapped(const PlayerId pid) const { const auto mapping_matches_player_id = [pid](const PlayerId& mapping) { return mapping == pid; }; - return std::ranges::any_of(m_pad_map, mapping_matches_player_id) || - std::ranges::any_of(m_wiimote_map, mapping_matches_player_id); + return std::ranges::any_of(m_net_settings.pad_map, mapping_matches_player_id) || + std::ranges::any_of(m_net_settings.wiimote_map, mapping_matches_player_id); } bool NetPlayClient::IsLocalPlayer(const PlayerId pid) const @@ -2502,7 +2448,7 @@ void NetPlayClient::SendGameStatus() m_dialog->FindGameFile(m_selected_game, &result); for (size_t i = 0; i < 4; ++i) { - if (m_gba_config[i].enabled && m_gba_config[i].has_rom && + if (m_net_settings.gba_config[i].enabled && m_net_settings.gba_config[i].has_rom && m_net_settings.gba_rom_paths[i].empty()) { result = SyncIdentifierComparison::DifferentGame; @@ -2615,17 +2561,17 @@ void NetPlayClient::ComputeGameDigest(const SyncIdentifier& sync_identifier) const PadMappingArray& NetPlayClient::GetPadMapping() const { - return m_pad_map; + return m_net_settings.pad_map; } const GBAConfigArray& NetPlayClient::GetGBAConfig() const { - return m_gba_config; + return m_net_settings.gba_config; } const PadMappingArray& NetPlayClient::GetWiimoteMapping() const { - return m_wiimote_map; + return m_net_settings.wiimote_map; } void NetPlayClient::AdjustPadBufferSize(const unsigned int size) diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index 10fd3bc94373..2eed9e45a068 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -232,10 +232,6 @@ class NetPlayClient : public Common::TraversalClientClient u32 m_current_game = 0; - PadMappingArray m_pad_map{}; - GBAConfigArray m_gba_config{}; - PadMappingArray m_wiimote_map{}; - bool m_is_recording = false; private: @@ -261,7 +257,6 @@ class NetPlayClient : public Common::TraversalClientClient bool AddLocalWiimoteToBuffer(int local_wiimote, const WiimoteEmu::SerializedWiimoteState& state, sf::Packet& packet); - void UpdateDevices(); void AddPadStateToPacket(int in_game_pad, const GCPadStatus& np, sf::Packet& packet); void AddWiimoteStateToPacket(int in_game_pad, const WiimoteEmu::SerializedWiimoteState& np, sf::Packet& packet); diff --git a/Source/Core/Core/NetPlayProto.h b/Source/Core/Core/NetPlayProto.h index d7df4e279a38..0cc897ebbde4 100644 --- a/Source/Core/Core/NetPlayProto.h +++ b/Source/Core/Core/NetPlayProto.h @@ -29,6 +29,20 @@ enum class CPUCore; namespace NetPlay { +struct GBAConfig +{ + bool enabled = false; + bool has_rom = false; + std::string title; + std::array hash{}; +}; + +using PlayerId = u8; +using FrameNum = u32; +using PadIndex = s8; +using PadMappingArray = std::array; +using GBAConfigArray = std::array; + struct NetSettings { bool cpu_thread = false; @@ -111,8 +125,14 @@ struct NetSettings Sram sram; + // These are sent separately from the other settings + PadMappingArray pad_map{}; + GBAConfigArray gba_config{}; + PadMappingArray wiimote_map{}; + // These aren't sent over the network directly bool is_hosting = false; + PlayerId local_player_id; std::array gba_rom_paths{}; }; @@ -234,19 +254,6 @@ enum : u8 CHANNEL_COUNT }; -using PlayerId = u8; -using FrameNum = u32; -using PadIndex = s8; -using PadMappingArray = std::array; -struct GBAConfig -{ - bool enabled = false; - bool has_rom = false; - std::string title; - std::array hash{}; -}; -using GBAConfigArray = std::array; - struct PadDetails { std::string player_name{}; diff --git a/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp b/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp index 4ec4da481837..2a932216d7a3 100644 --- a/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp +++ b/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp @@ -15,7 +15,6 @@ #include "Core/ConfigManager.h" #include "Core/Core.h" -#include "Core/HW/SI/SI.h" #include "Core/HW/SI/SI_Device.h" #include "Core/NetPlayProto.h" #include "Core/System.h" @@ -190,17 +189,11 @@ void GamecubeControllersWidget::SaveSettings() { Config::ConfigChangeCallbackGuard config_guard; - auto& system = Core::System::GetInstance(); for (size_t i = 0; i < m_gc_groups.size(); ++i) { const SerialInterface::SIDevices si_device = FromGCMenuIndex(m_gc_controller_boxes[i]->currentIndex()); Config::SetBaseOrCurrent(Config::GetInfoForSIDevice(static_cast(i)), si_device); - - if (Core::IsRunning(system)) - { - system.GetSerialInterface().ChangeDevice(si_device, static_cast(i)); - } } } From e68a464efd079f442c7abfd7884841c4adda8747 Mon Sep 17 00:00:00 2001 From: Martino Fontana Date: Thu, 12 Mar 2026 22:02:31 +0100 Subject: [PATCH 095/270] Jit64: Early flush in ComputeRC even when `needs_test` Example: ``` nand. r1, r2, r3 ``` Assuming that none of these registers will be used again: - Before #14278, all of them would be flushed on both sides of the branch. - After #14278, r2 and r3 would have been flushed earlier, but r1 won't. - With this, r1 is flushed in ComputeRC. --- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 21 ++++++------------- .../PowerPC/Jit64/RegCache/JitRegCache.cpp | 4 ++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index 978951118cd7..c77b831229a5 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -182,23 +182,14 @@ void Jit64::ComputeRC(preg_t preg, bool needs_test, bool needs_sext) if (CheckMergedBranch(0)) { + // If the output operand to the cmp/rc op we're merging with the branch isn't used anymore, it'd + // be better to flush it here so that we don't have to flush it on both sides of the branch. + // The flush is before the TEST so that it can macro-op fusion with the conditional branch. + gpr.Flush(~js.op->gprWillBeWritten & js.op->regsOut, RegCache::FlushMode::Undirty); if (needs_test) - { TEST(32, arg, arg); - arg.Unlock(); - } - else - { - // If an operand to the cmp/rc op we're merging with the branch isn't used anymore, it'd be - // better to flush it here so that we don't have to flush it on both sides of the branch. - // We don't want to do this if a test is needed though, because it would interrupt macro-op - // fusion. - arg.Unlock(); - gpr.Flush(~(js.op->gprWillBeRead | js.op->gprWillBeWritten) & - (js.op->regsIn | js.op->regsOut), - RegCache::FlushMode::Full); - gpr.Flush(~js.op->gprWillBeWritten & js.op->regsOut, RegCache::FlushMode::Undirty); - } + + arg.Unlock(); DoMergedBranchCondition(); } } diff --git a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp index 592813011d4b..2700f31232d4 100644 --- a/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp +++ b/Source/Core/Core/PowerPC/Jit64/RegCache/JitRegCache.cpp @@ -338,8 +338,8 @@ void RegCache::Flush(BitSet32 pregs, FlushMode mode, for (preg_t i : pregs) { - ASSERT_MSG(DYNA_REC, !m_regs[i].IsLocked(), "Someone forgot to unlock PPC reg {} (X64 reg {}).", - i, std::to_underlying(RX(i))); + ASSERT_MSG(DYNA_REC, mode != FlushMode::Full || !m_regs[i].IsLocked(), + "Someone forgot to unlock PPC reg {} (X64 reg {}).", i, std::to_underlying(RX(i))); ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Register transaction is in progress for {}!", i); From 2aba483a056aa35e0443de8e4da98a37411a7f1e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 28 Jun 2026 15:18:31 +0200 Subject: [PATCH 096/270] State: Use proper path encoding conversion for OSD messages Otherwise Windows will use the system code page. --- Source/Core/Core/State.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 345e1537e539..765e0d6906bb 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -28,6 +28,7 @@ #include "Common/IOFile.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" +#include "Common/StringUtil.h" #include "Common/Thread.h" #include "Common/TimeUtil.h" #include "Common/TransferableSharedMutex.h" @@ -463,8 +464,9 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat } else { - const std::filesystem::path temp_path(filename); - Core::DisplayMessage(fmt::format("Saved State to {}", temp_path.filename()), 2000); + const std::filesystem::path temp_path(StringToPath(filename)); + Core::DisplayMessage(fmt::format("Saved State to {}", PathToString(temp_path.filename())), + 2000); } } @@ -841,8 +843,9 @@ static void LoadAsFromCore(Core::System& system, std::string filename) { if (loaded_successfully) { - const std::filesystem::path temp_filename(filename); - Core::DisplayMessage(fmt::format("Loaded State from {}", temp_filename.filename()), 2000); + const std::filesystem::path temp_filename(StringToPath(filename)); + Core::DisplayMessage( + fmt::format("Loaded State from {}", PathToString(temp_filename.filename())), 2000); if (File::Exists(filename + ".dtm")) { movie.LoadInput(filename + ".dtm"); From b1d81af9cb1dcf722691be952e4a31caff49c13f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 29 Jun 2026 18:47:10 +0200 Subject: [PATCH 097/270] Android: Fix release build We don't need to see Compose previews when making a release build, but androidx.compose.ui.tooling.preview.Preview is referenced in the source code nonetheless, so the build system needs to know what it is. --- Source/Android/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 11433e81604f..11473af09f9c 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -169,8 +169,8 @@ dependencies { implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.ui) - debugImplementation(libs.androidx.compose.ui.tooling) - debugImplementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) } fun getGitVersion(): String { From 6825a4a954ced1611cb9a4e10fa2db7bd6955153 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Wed, 1 Jul 2026 15:59:46 +0200 Subject: [PATCH 098/270] Enable USE_UPNP in android builds The UI already exists in the Android netplay menu so just need to enable it in the build. --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dacb51c3820f..48e37f83dcdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -447,7 +447,6 @@ if(ANDROID) # but not as a shared library. We want an executable. set(ANDROID 0) endif() - set(USE_UPNP 0) set(ENABLE_QT 0) set(USE_DISCORD_PRESENCE 0) From 1a62cdca0e587a31eeae5494d22223976f7dbead Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Tue, 5 May 2026 14:22:22 -0700 Subject: [PATCH 099/270] Windows: Fix command line output not displaying when using Git Bash Check if Dolphin already has a handle for stdout or stderr before trying to attach to the console of the parent process. Previously, running Dolphin via Git Bash with options like `--help` or `--version` wouldn't produce any output. This was the result of the fix (1ce75ce21706a8dd9662e77ac9d09de1976f4733) for https://bugs.dolphin-emu.org/issues/11042, before which output did work for `Git Bash` but didn't for `Command Prompt` or `PowerShell`. --- Source/Core/DolphinQt/Main.cpp | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index b223819e7a35..86947ccf1b81 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -120,12 +120,27 @@ static bool QtMsgAlertHandler(const char* caption, const char* text, bool yes_no int main(int argc, char* argv[]) { #ifdef _WIN32 - const bool console_attached = AttachConsole(ATTACH_PARENT_PROCESS) != FALSE; - HANDLE stdout_handle = ::GetStdHandle(STD_OUTPUT_HANDLE); - if (console_attached && stdout_handle) + const HANDLE stdout_handle = ::GetStdHandle(STD_OUTPUT_HANDLE); + const HANDLE stderr_handle = ::GetStdHandle(STD_ERROR_HANDLE); + + const bool is_invalid_stdout_handle = + stdout_handle == nullptr || stdout_handle == INVALID_HANDLE_VALUE; + const bool is_invalid_stderr_handle = + stderr_handle == nullptr || stderr_handle == INVALID_HANDLE_VALUE; + + // If we already have a console don't try to use one from our parent. This happens when running + // Dolphin using `Git Bash`. + if (is_invalid_stdout_handle && is_invalid_stderr_handle) { - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); + // See if the parent process has a console we can use (which happens when Dolphin is launched + // via `Command Prompt` or `PowerShell`). If this fails Dolphin was probably launched via the + // GUI, in which case we don't want a console anyway. + const bool attached_to_parent_console = AttachConsole(ATTACH_PARENT_PROCESS) != FALSE; + if (attached_to_parent_console) + { + static_cast(freopen("CONOUT$", "w", stdout)); + static_cast(freopen("CONOUT$", "w", stderr)); + } } #endif From 5f372d6b1df2a6c87cf54e4f6eeb5c0e12383739 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 19 Jun 2026 11:37:04 +0200 Subject: [PATCH 100/270] VideoCommon: Reduce padding in PresentInfo This makes the struct 8 bytes smaller. --- Source/Core/VideoCommon/Present.cpp | 2 +- Source/Core/VideoCommon/VideoEvents.h | 40 +++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index 7c627306676e..f74b9a8fa4b6 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -234,9 +234,9 @@ void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_ PresentInfo present_info{ .frame_count = m_frame_count++, .present_count = m_present_count++, - .reason = PresentInfo::PresentReason::Immediate, .emulated_timestamp = ticks, .intended_present_time = m_next_swap_estimated_time, + .reason = PresentInfo::PresentReason::Immediate, }; auto& video_events = GetVideoEvents(); diff --git a/Source/Core/VideoCommon/VideoEvents.h b/Source/Core/VideoCommon/VideoEvents.h index fe30aa251b8d..f07ac631e450 100644 --- a/Source/Core/VideoCommon/VideoEvents.h +++ b/Source/Core/VideoCommon/VideoEvents.h @@ -16,6 +16,23 @@ class System; struct PresentInfo { + enum class PresentTimeAccuracy + { + // The Driver/OS has given us an exact timestamp of when the first line of the frame started + // scanning out to the monitor + PresentOnScreenExact, + + // An approximate timestamp of scanout. + PresentOnScreen, + + // Dolphin doesn't have visibility of the present time. But the present operation has + // been queued with the GPU driver and will happen in the near future. + PresentInProgress, + + // Not implemented + Unimplemented, + }; + enum class PresentReason { Immediate, // FIFO is Presenting the XFB immediately, straight after the XFB copy @@ -30,9 +47,6 @@ struct PresentInfo // never goes backwards. u64 present_count = 0; - // The frame is identical to the previous frame - PresentReason reason = PresentReason::Immediate; - // The exact emulated time of the when real hardware would have presented this frame u64 emulated_timestamp = 0; @@ -41,26 +55,12 @@ struct PresentInfo // AfterPresent only: The actual time the frame was presented TimePoint actual_present_time{}; - enum class PresentTimeAccuracy - { - // The Driver/OS has given us an exact timestamp of when the first line of the frame started - // scanning out to the monitor - PresentOnScreenExact, - - // An approximate timestamp of scanout. - PresentOnScreen, - - // Dolphin doesn't have visibility of the present time. But the present operation has - // been queued with the GPU driver and will happen in the near future. - PresentInProgress, - - // Not implemented - Unimplemented, - }; - // Accuracy of actual_present_time PresentTimeAccuracy present_time_accuracy = PresentTimeAccuracy::Unimplemented; + // Where the presentation of the frame was triggered from + PresentReason reason = PresentReason::Immediate; + std::vector xfb_copy_hashes; }; From c958dbc46cd93b24aa9c63332739b4040880c711 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 19 Jun 2026 12:27:38 +0200 Subject: [PATCH 101/270] VideoCommon: Use XFB for internal resolution stats fb4ff3e added a statistics option to show the internal resolution, but it just showed the total size of the EFB (which is always 640x528) times the IR scale, so it didn't convey any useful information. This commit instead makes the option use the size of the last XFB copy (not multiplied by the IR scale), which changes based on the game's rendering resolution. --- Source/Core/Core/Core.cpp | 3 +++ .../Core/VideoCommon/PerformanceMetrics.cpp | 25 ++++++------------- Source/Core/VideoCommon/PerformanceMetrics.h | 10 ++++++-- Source/Core/VideoCommon/Present.cpp | 4 +++ Source/Core/VideoCommon/VideoEvents.h | 3 +++ 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index cebcc748ec4b..1b50796e34e5 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -863,6 +863,9 @@ void Callback_FramePresented(const PresentInfo& present_info) present_info.actual_present_time - present_info.intended_present_time; perf_metrics.SetLatestFramePresentationOffset(presentation_offset); + perf_metrics.SetLatestFrameBufferSize(present_info.frame_buffer_width, + present_info.frame_buffer_height); + if (present_info.reason == PresentInfo::PresentReason::VideoInterfaceDuplicate) return; diff --git a/Source/Core/VideoCommon/PerformanceMetrics.cpp b/Source/Core/VideoCommon/PerformanceMetrics.cpp index fccc8ae38e4e..b94fc6fadeb9 100644 --- a/Source/Core/VideoCommon/PerformanceMetrics.cpp +++ b/Source/Core/VideoCommon/PerformanceMetrics.cpp @@ -11,7 +11,6 @@ #include "Common/HookableEvent.h" #include "Core/Config/GraphicsSettings.h" #include "Core/Core.h" -#include "VideoCommon/FramebufferManager.h" #include "VideoCommon/VideoConfig.h" PerformanceMetrics::PerformanceMetrics() @@ -89,20 +88,6 @@ double PerformanceMetrics::GetFPS() const return m_fps_counter.GetHzAvg(); } -u32 PerformanceMetrics::GetEFBWidth() const -{ - if (g_framebuffer_manager) - return g_framebuffer_manager->GetEFBWidth(); - return 0; -} - -u32 PerformanceMetrics::GetEFBHeight() const -{ - if (g_framebuffer_manager) - return g_framebuffer_manager->GetEFBHeight(); - return 0; -} - double PerformanceMetrics::GetVPS() const { return m_vps_counter.GetHzAvg(); @@ -123,6 +108,11 @@ void PerformanceMetrics::SetLatestFramePresentationOffset(DT offset) m_frame_presentation_offset.store(offset, std::memory_order_relaxed); } +void PerformanceMetrics::SetLatestFrameBufferSize(u32 width, u32 height) +{ + m_frame_buffer_size.store(FrameBufferSize{width, height}, std::memory_order_relaxed); +} + void PerformanceMetrics::DrawImGuiStats(const float backbuffer_scale) { m_vps_counter.UpdateStats(); @@ -139,8 +129,6 @@ void PerformanceMetrics::DrawImGuiStats(const float backbuffer_scale) const double fps = GetFPS(); const double vps = GetVPS(); const double speed = GetSpeed(); - const u32 width = GetEFBWidth(); - const u32 height = GetEFBHeight(); static ImVec2 last_display_size(-1.0f, -1.0f); @@ -350,7 +338,8 @@ void PerformanceMetrics::DrawImGuiStats(const float backbuffer_scale) clamp_window_position(); - ImGui::TextColored(ImVec4(r, g, b, 1.0f), "Res: %ux%u", width, height); + const FrameBufferSize size = m_frame_buffer_size.load(std::memory_order_relaxed); + ImGui::TextColored(ImVec4(r, g, b, 1.0f), "XFB res: %ux%u", size.width, size.height); } ImGui::End(); } diff --git a/Source/Core/VideoCommon/PerformanceMetrics.h b/Source/Core/VideoCommon/PerformanceMetrics.h index 538360a05b96..b24c6f665988 100644 --- a/Source/Core/VideoCommon/PerformanceMetrics.h +++ b/Source/Core/VideoCommon/PerformanceMetrics.h @@ -41,15 +41,20 @@ class PerformanceMetrics double GetVPS() const; double GetSpeed() const; double GetMaxSpeed() const; - u32 GetEFBWidth() const; - u32 GetEFBHeight() const; // Call from any thread. void SetLatestFramePresentationOffset(DT offset); + void SetLatestFrameBufferSize(u32 width, u32 height); // ImGui Functions void DrawImGuiStats(const float backbuffer_scale); private: + struct FrameBufferSize + { + u32 width = 0; + u32 height = 0; + }; + PerformanceTracker m_fps_counter{"render_times.txt"}; PerformanceTracker m_vps_counter{"vblank_times.txt"}; @@ -59,6 +64,7 @@ class PerformanceMetrics std::atomic m_max_speed{}; std::atomic
m_frame_presentation_offset{}; + std::atomic m_frame_buffer_size{}; struct PerfSample { diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index f74b9a8fa4b6..41d05f36520e 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -174,6 +174,8 @@ void Presenter::ViSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, .present_count = m_present_count++, .emulated_timestamp = ticks, .intended_present_time = presentation_time, + .frame_buffer_width = fb_width, + .frame_buffer_height = fb_height, }; if (is_duplicate) @@ -237,6 +239,8 @@ void Presenter::ImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_ .emulated_timestamp = ticks, .intended_present_time = m_next_swap_estimated_time, .reason = PresentInfo::PresentReason::Immediate, + .frame_buffer_width = fb_width, + .frame_buffer_height = fb_height, }; auto& video_events = GetVideoEvents(); diff --git a/Source/Core/VideoCommon/VideoEvents.h b/Source/Core/VideoCommon/VideoEvents.h index f07ac631e450..f037dc266d4f 100644 --- a/Source/Core/VideoCommon/VideoEvents.h +++ b/Source/Core/VideoCommon/VideoEvents.h @@ -61,6 +61,9 @@ struct PresentInfo // Where the presentation of the frame was triggered from PresentReason reason = PresentReason::Immediate; + u32 frame_buffer_width; + u32 frame_buffer_height; + std::vector xfb_copy_hashes; }; From 6f72de3579c9420e8255649c76ad234555cd50c9 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 19 Jun 2026 12:31:46 +0200 Subject: [PATCH 102/270] VideoCommon: Show internal resolution stats below VPS It looked a bit out of place wedged in between the FPS and VPS. --- .../Core/VideoCommon/PerformanceMetrics.cpp | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Source/Core/VideoCommon/PerformanceMetrics.cpp b/Source/Core/VideoCommon/PerformanceMetrics.cpp index b94fc6fadeb9..98efa64108ed 100644 --- a/Source/Core/VideoCommon/PerformanceMetrics.cpp +++ b/Source/Core/VideoCommon/PerformanceMetrics.cpp @@ -323,50 +323,50 @@ void PerformanceMetrics::DrawImGuiStats(const float backbuffer_scale) ImGui::End(); } - if (g_ActiveConfig.bShowInternalResolution) + if (g_ActiveConfig.bShowVPS || g_ActiveConfig.bShowVTimes) { + // Position in the top-right corner of the screen. ImGui::SetNextWindowPos(ImVec2(window_x, window_y), set_next_position_condition, ImVec2(1.0f, 0.0f)); ImGui::SetNextWindowBgAlpha(bg_alpha); - if (ImGui::Begin("ResolutionStats", nullptr, imgui_flags)) + if (ImGui::Begin("VPSStats", nullptr, imgui_flags)) { if (stack_vertically) window_y += ImGui::GetWindowHeight() + window_padding; else window_x -= ImGui::GetWindowWidth() + window_padding; - clamp_window_position(); - - const FrameBufferSize size = m_frame_buffer_size.load(std::memory_order_relaxed); - ImGui::TextColored(ImVec4(r, g, b, 1.0f), "XFB res: %ux%u", size.width, size.height); + if (g_ActiveConfig.bShowVPS) + ImGui::TextColored(ImVec4(r, g, b, 1.0f), "VPS:%7.2lf", vps); + if (g_ActiveConfig.bShowVTimes) + { + ImGui::TextColored(ImVec4(r, g, b, 1.0f), "dt:%6.2lfms", + DT_ms(m_vps_counter.GetDtAvg()).count()); + ImGui::TextColored(ImVec4(r, g, b, 1.0f), " ±:%6.2lfms", + DT_ms(m_vps_counter.GetDtStd()).count()); + } } ImGui::End(); } - if (g_ActiveConfig.bShowVPS || g_ActiveConfig.bShowVTimes) + if (g_ActiveConfig.bShowInternalResolution) { - // Position in the top-right corner of the screen. ImGui::SetNextWindowPos(ImVec2(window_x, window_y), set_next_position_condition, ImVec2(1.0f, 0.0f)); ImGui::SetNextWindowBgAlpha(bg_alpha); - if (ImGui::Begin("VPSStats", nullptr, imgui_flags)) + if (ImGui::Begin("ResolutionStats", nullptr, imgui_flags)) { if (stack_vertically) window_y += ImGui::GetWindowHeight() + window_padding; else window_x -= ImGui::GetWindowWidth() + window_padding; + clamp_window_position(); - if (g_ActiveConfig.bShowVPS) - ImGui::TextColored(ImVec4(r, g, b, 1.0f), "VPS:%7.2lf", vps); - if (g_ActiveConfig.bShowVTimes) - { - ImGui::TextColored(ImVec4(r, g, b, 1.0f), "dt:%6.2lfms", - DT_ms(m_vps_counter.GetDtAvg()).count()); - ImGui::TextColored(ImVec4(r, g, b, 1.0f), " ±:%6.2lfms", - DT_ms(m_vps_counter.GetDtStd()).count()); - } + + const FrameBufferSize size = m_frame_buffer_size.load(std::memory_order_relaxed); + ImGui::TextColored(ImVec4(r, g, b, 1.0f), "XFB res: %ux%u", size.width, size.height); } ImGui::End(); } From aff4d18d5932e9cf65d0fe8b5752fd709c302a01 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 19 Jun 2026 12:35:56 +0200 Subject: [PATCH 103/270] Move the code for the show internal resolution setting fb4ff3e put all the code for the show internal resolution setting right next to the code for the show FPS setting, presumably because an earlier version of that commit had them next to each other in the GUI. It makes more conceptual sense to put the code next to the code for the show statistics settings, matching what the GUI looks like now. --- Source/Core/Core/Config/GraphicsSettings.cpp | 4 ++-- Source/Core/Core/Config/GraphicsSettings.h | 2 +- .../DolphinQt/Settings/OnScreenDisplayPane.cpp | 15 +++++++-------- .../Core/DolphinQt/Settings/OnScreenDisplayPane.h | 2 +- Source/Core/VideoCommon/VideoConfig.cpp | 2 +- Source/Core/VideoCommon/VideoConfig.h | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Source/Core/Core/Config/GraphicsSettings.cpp b/Source/Core/Core/Config/GraphicsSettings.cpp index fb7f6a872b8c..f91b87e490a0 100644 --- a/Source/Core/Core/Config/GraphicsSettings.cpp +++ b/Source/Core/Core/Config/GraphicsSettings.cpp @@ -43,8 +43,6 @@ const Info GFX_CROP_CUSTOM_BOTTOM{{System::GFX, "Settings", "CropCustomBott const Info GFX_SAFE_TEXTURE_CACHE_COLOR_SAMPLES{ {System::GFX, "Settings", "SafeTextureCacheColorSamples"}, 128}; const Info GFX_SHOW_FPS{{System::GFX, "Settings", "ShowFPS"}, false}; -const Info GFX_SHOW_INTERNAL_RESOLUTION{{System::GFX, "Settings", "ShowInternalResolution"}, - false}; const Info GFX_SHOW_FTIMES{{System::GFX, "Settings", "ShowFTimes"}, false}; const Info GFX_SHOW_VPS{{System::GFX, "Settings", "ShowVPS"}, false}; const Info GFX_SHOW_VTIMES{{System::GFX, "Settings", "ShowVTimes"}, false}; @@ -61,6 +59,8 @@ const Info GFX_LOG_RENDER_TIME_TO_FILE{{System::GFX, "Settings", "LogRende const Info GFX_OVERLAY_STATS{{System::GFX, "Settings", "OverlayStats"}, false}; const Info GFX_OVERLAY_PROJ_STATS{{System::GFX, "Settings", "OverlayProjStats"}, false}; const Info GFX_OVERLAY_SCISSOR_STATS{{System::GFX, "Settings", "OverlayScissorStats"}, false}; +const Info GFX_SHOW_INTERNAL_RESOLUTION{{System::GFX, "Settings", "ShowInternalResolution"}, + false}; const Info GFX_DUMP_TEXTURES{{System::GFX, "Settings", "DumpTextures"}, false}; const Info GFX_DUMP_MIP_TEXTURES{{System::GFX, "Settings", "DumpMipTextures"}, true}; const Info GFX_DUMP_BASE_TEXTURES{{System::GFX, "Settings", "DumpBaseTextures"}, true}; diff --git a/Source/Core/Core/Config/GraphicsSettings.h b/Source/Core/Core/Config/GraphicsSettings.h index 831e04422b8d..502d2f076438 100644 --- a/Source/Core/Core/Config/GraphicsSettings.h +++ b/Source/Core/Core/Config/GraphicsSettings.h @@ -47,7 +47,6 @@ extern const Info GFX_CROP_CUSTOM_RIGHT; extern const Info GFX_CROP_CUSTOM_BOTTOM; extern const Info GFX_SAFE_TEXTURE_CACHE_COLOR_SAMPLES; extern const Info GFX_SHOW_FPS; -extern const Info GFX_SHOW_INTERNAL_RESOLUTION; extern const Info GFX_SHOW_FTIMES; extern const Info GFX_SHOW_VPS; extern const Info GFX_SHOW_VTIMES; @@ -62,6 +61,7 @@ extern const Info GFX_LOG_RENDER_TIME_TO_FILE; extern const Info GFX_OVERLAY_STATS; extern const Info GFX_OVERLAY_PROJ_STATS; extern const Info GFX_OVERLAY_SCISSOR_STATS; +extern const Info GFX_SHOW_INTERNAL_RESOLUTION; extern const Info GFX_DUMP_TEXTURES; extern const Info GFX_DUMP_MIP_TEXTURES; extern const Info GFX_DUMP_BASE_TEXTURES; diff --git a/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp b/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp index 37239b9c7473..cc8248f15b01 100644 --- a/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp +++ b/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp @@ -43,8 +43,6 @@ void OnScreenDisplayPane::CreateLayout() performance_box->setLayout(performance_layout); m_show_fps = new ConfigBool(tr("Show FPS"), Config::GFX_SHOW_FPS); - m_show_internal_resolution = - new ConfigBool(tr("Show Internal Resolution"), Config::GFX_SHOW_INTERNAL_RESOLUTION); m_show_ftimes = new ConfigBool(tr("Show Frame Times"), Config::GFX_SHOW_FTIMES); m_show_vps = new ConfigBool(tr("Show VPS"), Config::GFX_SHOW_VPS); m_show_vtimes = new ConfigBool(tr("Show VBlank Times"), Config::GFX_SHOW_VTIMES); @@ -103,6 +101,8 @@ void OnScreenDisplayPane::CreateLayout() m_show_statistics = new ConfigBool(tr("Show Statistics"), Config::GFX_OVERLAY_STATS); m_show_proj_statistics = new ConfigBool(tr("Show Projection Statistics"), Config::GFX_OVERLAY_PROJ_STATS); + m_show_internal_resolution = + new ConfigBool(tr("Show Internal Resolution"), Config::GFX_SHOW_INTERNAL_RESOLUTION); debug_layout->addWidget(m_show_statistics, 0, 0); debug_layout->addWidget(m_show_proj_statistics, 0, 1); @@ -153,11 +153,6 @@ void OnScreenDisplayPane::AddDescriptions() QT_TR_NOOP("Shows the number of distinct frames rendered per second as a measure of " "visual smoothness.

If unsure, leave this " "unchecked."); - - static const char TR_SHOW_INTERNAL_RESOLUTION_DESCRIPTION[] = - QT_TR_NOOP("Shows the internal resolution in pixels, as a product of " - "width and height.

If unsure, leave this " - "unchecked."); static const char TR_SHOW_FTIMES_DESCRIPTION[] = QT_TR_NOOP("Shows the average time in ms between each distinct rendered frame alongside " "the standard deviation.

If unsure, leave this " @@ -225,12 +220,15 @@ void OnScreenDisplayPane::AddDescriptions() static const char TR_SHOW_PROJ_STATS_DESCRIPTION[] = QT_TR_NOOP("Shows various projection statistics.

If unsure, " "leave this unchecked."); + static const char TR_SHOW_INTERNAL_RESOLUTION_DESCRIPTION[] = + QT_TR_NOOP("Shows the internal resolution in pixels, as a product of " + "width and height.

If unsure, leave this " + "unchecked."); m_enable_osd->SetDescription(tr(TR_ENABLE_OSD_DESCRIPTION)); m_font_size->SetDescription(tr(TR_OSD_FONT_SIZE_DESCRIPTION)); m_show_fps->SetDescription(tr(TR_SHOW_FPS_DESCRIPTION)); - m_show_internal_resolution->SetDescription(tr(TR_SHOW_INTERNAL_RESOLUTION_DESCRIPTION)); m_show_ftimes->SetDescription(tr(TR_SHOW_FTIMES_DESCRIPTION)); m_show_vps->SetDescription(tr(TR_SHOW_VPS_DESCRIPTION)); m_show_vtimes->SetDescription(tr(TR_SHOW_VTIMES_DESCRIPTION)); @@ -251,4 +249,5 @@ void OnScreenDisplayPane::AddDescriptions() m_show_statistics->SetDescription(tr(TR_SHOW_STATS_DESCRIPTION)); m_show_proj_statistics->SetDescription(tr(TR_SHOW_PROJ_STATS_DESCRIPTION)); + m_show_internal_resolution->SetDescription(tr(TR_SHOW_INTERNAL_RESOLUTION_DESCRIPTION)); } diff --git a/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.h b/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.h index 27653d009c56..8ae4e9bc76cb 100644 --- a/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.h +++ b/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.h @@ -25,7 +25,6 @@ class OnScreenDisplayPane final : public QWidget // Performance ConfigBool* m_show_fps; - ConfigBool* m_show_internal_resolution; ConfigBool* m_show_ftimes; ConfigBool* m_show_vps; ConfigBool* m_show_vtimes; @@ -49,4 +48,5 @@ class OnScreenDisplayPane final : public QWidget // Debug ConfigBool* m_show_statistics; ConfigBool* m_show_proj_statistics; + ConfigBool* m_show_internal_resolution; }; diff --git a/Source/Core/VideoCommon/VideoConfig.cpp b/Source/Core/VideoCommon/VideoConfig.cpp index 448f44774143..c56f32c00cba 100644 --- a/Source/Core/VideoCommon/VideoConfig.cpp +++ b/Source/Core/VideoCommon/VideoConfig.cpp @@ -106,7 +106,6 @@ void VideoConfig::Refresh() iCropCustomBottom = Config::Get(Config::GFX_CROP_CUSTOM_BOTTOM); iSafeTextureCache_ColorSamples = Config::Get(Config::GFX_SAFE_TEXTURE_CACHE_COLOR_SAMPLES); bShowFPS = Config::Get(Config::GFX_SHOW_FPS); - bShowInternalResolution = Config::Get(Config::GFX_SHOW_INTERNAL_RESOLUTION); bShowFTimes = Config::Get(Config::GFX_SHOW_FTIMES); bShowVPS = Config::Get(Config::GFX_SHOW_VPS); bShowVTimes = Config::Get(Config::GFX_SHOW_VTIMES); @@ -118,6 +117,7 @@ void VideoConfig::Refresh() bOverlayStats = Config::Get(Config::GFX_OVERLAY_STATS); bOverlayProjStats = Config::Get(Config::GFX_OVERLAY_PROJ_STATS); bOverlayScissorStats = Config::Get(Config::GFX_OVERLAY_SCISSOR_STATS); + bShowInternalResolution = Config::Get(Config::GFX_SHOW_INTERNAL_RESOLUTION); bDumpTextures = Config::Get(Config::GFX_DUMP_TEXTURES); bDumpMipmapTextures = Config::Get(Config::GFX_DUMP_MIP_TEXTURES); bDumpBaseTextures = Config::Get(Config::GFX_DUMP_BASE_TEXTURES); diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index ec06f4a606dc..e16015491b3f 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -250,7 +250,6 @@ struct VideoConfig final // Information bool bShowFPS = false; - bool bShowInternalResolution = false; bool bShowFTimes = false; bool bShowVPS = false; bool bShowVTimes = false; @@ -261,6 +260,7 @@ struct VideoConfig final bool bOverlayStats = false; bool bOverlayProjStats = false; bool bOverlayScissorStats = false; + bool bShowInternalResolution = false; bool bTexFmtOverlayEnable = false; bool bTexFmtOverlayCenter = false; bool bLogRenderTimeToFile = false; From db77b117d9480f528b435c29f96c2bbc264a2610 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 19 Jun 2026 10:53:36 +0200 Subject: [PATCH 104/270] Android: Add missing graphics debug settings These are available in DolphinQt, so let's make them available in the Android GUI too. --- .../features/settings/model/BooleanSetting.kt | 12 ++++++++++++ .../settings/ui/SettingsFragmentPresenter.kt | 16 ++++++++++++++++ .../Android/app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 30 insertions(+) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index 1a03eac3f440..68c859082cd2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -700,6 +700,18 @@ enum class BooleanSetting( false ), GFX_OVERLAY_STATS(Settings.FILE_GFX, Settings.SECTION_GFX_SETTINGS, "OverlayStats", false), + GFX_OVERLAY_PROJ_STATS( + Settings.FILE_GFX, + Settings.SECTION_GFX_SETTINGS, + "OverlayProjStats", + false + ), + GFX_SHOW_INTERNAL_RESOLUTION( + Settings.FILE_GFX, + Settings.SECTION_GFX_SETTINGS, + "ShowInternalResolution", + false + ), GFX_DUMP_TEXTURES(Settings.FILE_GFX, Settings.SECTION_GFX_SETTINGS, "DumpTextures", false), GFX_DUMP_MIP_TEXTURES( Settings.FILE_GFX, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 78e36f7b42e2..08e73ea7dd64 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -2169,6 +2169,22 @@ class SettingsFragmentPresenter( R.string.leave_this_unchecked ) ) + sl.add( + SwitchSetting( + context, + BooleanSetting.GFX_OVERLAY_PROJ_STATS, + R.string.show_proj_stats, + R.string.leave_this_unchecked + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.GFX_SHOW_INTERNAL_RESOLUTION, + R.string.show_internal_resolution, + R.string.leave_this_unchecked + ) + ) sl.add( SwitchSetting( context, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 21a42e5939c0..71179b33b18f 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -374,6 +374,8 @@ Debugging Enable Wireframe Show Statistics + Show Projection Statistics + Show Internal Resolution Texture Format Overlay Enable API Validation Layers Dump EFB Target From be1df92153d94b276fee69182d4774e7bfc1b3f1 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 5 Jul 2026 10:20:15 +0200 Subject: [PATCH 105/270] Rename "Show Internal Resolution" to "Show XFB Resolution" To make it extra clear that this is measuring the XFB, not the EFB. --- Source/Android/app/src/main/res/values/strings.xml | 2 +- Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 71179b33b18f..07c5e7ebc003 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -375,7 +375,7 @@ Enable Wireframe Show Statistics Show Projection Statistics - Show Internal Resolution + Show XFB Resolution Texture Format Overlay Enable API Validation Layers Dump EFB Target diff --git a/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp b/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp index cc8248f15b01..2c8b55b899ed 100644 --- a/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp +++ b/Source/Core/DolphinQt/Settings/OnScreenDisplayPane.cpp @@ -102,7 +102,7 @@ void OnScreenDisplayPane::CreateLayout() m_show_proj_statistics = new ConfigBool(tr("Show Projection Statistics"), Config::GFX_OVERLAY_PROJ_STATS); m_show_internal_resolution = - new ConfigBool(tr("Show Internal Resolution"), Config::GFX_SHOW_INTERNAL_RESOLUTION); + new ConfigBool(tr("Show XFB Resolution"), Config::GFX_SHOW_INTERNAL_RESOLUTION); debug_layout->addWidget(m_show_statistics, 0, 0); debug_layout->addWidget(m_show_proj_statistics, 0, 1); @@ -221,8 +221,8 @@ void OnScreenDisplayPane::AddDescriptions() QT_TR_NOOP("Shows various projection statistics.

If unsure, " "leave this unchecked."); static const char TR_SHOW_INTERNAL_RESOLUTION_DESCRIPTION[] = - QT_TR_NOOP("Shows the internal resolution in pixels, as a product of " - "width and height.

If unsure, leave this " + QT_TR_NOOP("Shows the size of the emulated external frame buffer (XFB) in pixels, as a " + "product of width and height.

If unsure, leave this " "unchecked."); m_enable_osd->SetDescription(tr(TR_ENABLE_OSD_DESCRIPTION)); From f5d225691ed3bc56e2da589e7f548835bae090c9 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 27 Jun 2026 08:18:34 +0200 Subject: [PATCH 106/270] Android: Set USAGE_MEDIA for rumble This lets rumble work even if the user has turned off the Android setting Sound and vibration > Vibration and haptics > Interactive haptics > Touch feedback. Rumble can still be disabled through Dolphin's controller bindings and through in-game settings (GameCube) or SYSCONF (Wii). --- .../input/model/ControllerInterface.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt index 71b37ff51347..ceba48e4bf1a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt @@ -4,10 +4,12 @@ package org.dolphinemu.dolphinemu.features.input.model import android.content.Context import android.hardware.input.InputManager +import android.media.AudioAttributes import android.os.Build import android.os.Handler import android.os.HandlerThread import android.os.Looper +import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager @@ -180,10 +182,21 @@ object ControllerInterface { @Keep @JvmStatic private fun vibrate(vibrator: Vibrator) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + vibrator.vibrate( + VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE), + VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_MEDIA).build() + ) } else { - vibrator.vibrate(100) + val attributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate( + VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE), + attributes + ) + } else { + vibrator.vibrate(100, attributes) + } } } From 342115806beb93a1f14dd9e40b6839148d70b986 Mon Sep 17 00:00:00 2001 From: tommywaaf Date: Fri, 5 Jun 2026 12:37:30 -0400 Subject: [PATCH 107/270] Android: Route DolphinBar Balance Board to the Balance Board slot WiimoteScannerAndroid::FindAttachedWiimotes() added every device connected through the DolphinBar to the Wii Remote results, with a TODO noting that balance boards were not differentiated. As a result, a Wii Balance Board connected through the DolphinBar was always assigned to a Wii Remote slot (Player 1), and games that request a Balance Board never detected it. Use Wiimote::IsBalanceBoard() to route balance boards into the balance_boards results, matching the Linux and Windows scanner backends. Tested on an AYN Thor (Android 13) with a Mayflash DolphinBar in Mode 4 running Wii Fit Plus: the board is now detected in the Balance Board slot. --- Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp b/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp index e82f389a21aa..5e201d6ac24c 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp +++ b/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp @@ -46,10 +46,12 @@ auto WiimoteScannerAndroid::FindAttachedWiimotes() -> FindResults if (!wiimote->ConnectInternal()) continue; - // TODO: We make no attempt to differentiate balance boards here. - // wiimote->IsBalanceBoard() would probably be enough to do that. - - results.wii_remotes.emplace_back(std::move(wiimote)); + // Differentiate balance boards so they are routed to the balance + // board results rather than being treated as a regular Wii Remote. + if (wiimote->IsBalanceBoard()) + results.balance_boards.emplace_back(std::move(wiimote)); + else + results.wii_remotes.emplace_back(std::move(wiimote)); } } From 11cf49de069621d43b6cef47017ef4279efec637 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 12 May 2026 16:13:05 -0700 Subject: [PATCH 108/270] Android: Coloured netplay chat messages --- .../features/netplay/ui/NetplayScreen.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 54ea403eeb9d..d3a05d3fcfa6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.Intent import android.content.res.Configuration +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -87,6 +88,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import androidx.window.core.layout.WindowSizeClass import coil.compose.AsyncImage @@ -588,8 +590,10 @@ private fun Chat( fun LazyListScope.messages() { items(messages.size) { index -> + val message = messages[index] Text( - text = messages[index].message(context), + text = message.message(context), + color = message.color(), style = MaterialTheme.typography.bodyMedium.copy(lineHeight = 18.sp), modifier = Modifier .fillMaxWidth() @@ -1265,6 +1269,21 @@ private fun GameDigestPlayerRow( } } +@Composable +private fun NetplayMessage.color(): Color { + val isDark = isSystemInDarkTheme() + return when (this) { + is NetplayMessage.Chat -> Color.Unspecified + is NetplayMessage.GameChanged -> if (isDark) Color(0xFFCE93D8) else Color(0xFF8E24AA) + is NetplayMessage.HostInputAuthorityChanged -> if (isDark) Color(0xFF90CAF9) else Color( + 0xFF1565C0 + ) + + is NetplayMessage.BufferChanged -> if (isDark) Color(0xFF80CBC4) else Color(0xFF00897B) + is NetplayMessage.Desync -> if (isDark) Color(0xFFEF9A9A) else Color(0xFFC62828) + } +} + @Preview @Composable private fun NetplayScreenPreview() { From e222665eb0a67b8d66b3e0f48476a8e62987fed3 Mon Sep 17 00:00:00 2001 From: Craig Carnell <1188869+cscd98@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:34:24 +0100 Subject: [PATCH 109/270] mingw: lower case windows includes --- Source/Core/AudioCommon/CubebStream.cpp | 2 +- Source/Core/AudioCommon/CubebUtils.cpp | 2 +- Source/Core/AudioCommon/WASAPIStream.cpp | 2 +- Source/Core/AudioCommon/WASAPIStream.h | 2 +- Source/Core/Common/Arm64Emitter.cpp | 2 +- Source/Core/Common/ArmCPUDetect.cpp | 2 +- Source/Core/Common/CommonFuncs.cpp | 2 +- Source/Core/Common/CompatPatches.cpp | 2 +- Source/Core/Common/DynamicLibrary.cpp | 2 +- Source/Core/Common/FileSearch.cpp | 4 ++-- Source/Core/Common/FileUtil.cpp | 4 ++-- Source/Core/Common/LdrWatcher.cpp | 4 ++-- Source/Core/Common/Network.cpp | 2 +- Source/Core/Common/SocketContext.h | 2 +- Source/Core/Common/StringUtil.cpp | 2 +- Source/Core/Common/Thread.cpp | 2 +- Source/Core/Common/Timer.cpp | 2 +- Source/Core/Common/WindowsDevice.cpp | 2 +- Source/Core/Common/WindowsDevice.h | 5 ++--- Source/Core/Common/WindowsRegistry.cpp | 2 +- Source/Core/Common/WindowsRegistry.h | 2 +- Source/Core/Core/Config/DefaultLocale.cpp | 2 +- Source/Core/Core/DolphinAnalytics.cpp | 2 +- Source/Core/Core/HW/DSPHLE/UCodes/ROM.cpp | 2 +- Source/Core/Core/HW/DSPHLE/UCodes/UCodes.cpp | 2 +- Source/Core/Core/HW/EXI/BBA/BuiltIn.h | 2 +- Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h | 2 +- Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp | 2 +- Source/Core/Core/HW/WiimoteReal/IOWin.cpp | 10 +++++----- Source/Core/Core/IOS/FS/FileSystem.h | 2 +- Source/Core/Core/IOS/Network/ICMP.h | 2 +- Source/Core/Core/IOS/Network/Socket.h | 2 +- Source/Core/Core/IOS/USB/Emulated/Microphone.cpp | 2 +- Source/Core/Core/NetworkCaptureLogger.h | 2 +- Source/Core/Core/PowerPC/GDBStub.cpp | 2 +- Source/Core/DolphinNoGUI/MainNoGUI.cpp | 2 +- Source/Core/DolphinNoGUI/PlatformWin32.cpp | 2 +- Source/Core/DolphinQt/Main.cpp | 2 +- Source/Core/DolphinQt/RenderWidget.cpp | 2 +- .../ControllerInterface/DInput/XInputFilter.cpp | 4 ++-- .../ControllerInterface/DInput/XInputFilter.h | 2 +- .../ForceFeedback/ForceFeedbackDevice.h | 2 +- .../Core/InputCommon/ControllerInterface/SDL/SDL.cpp | 2 +- .../InputCommon/ControllerInterface/Win32/Win32.cpp | 2 +- .../InputCommon/ControllerInterface/XInput/XInput.h | 2 +- Source/Core/UICommon/AutoUpdate.cpp | 2 +- Source/Core/UpdaterCommon/UpdaterCommon.cpp | 2 +- Source/Core/WinUpdater/Platform.cpp | 2 +- Source/Core/WinUpdater/WinUI.cpp | 8 ++++---- Source/PCH/pch.h | 2 +- 50 files changed, 62 insertions(+), 63 deletions(-) diff --git a/Source/Core/AudioCommon/CubebStream.cpp b/Source/Core/AudioCommon/CubebStream.cpp index adc947b42674..0cdf7f3b9998 100644 --- a/Source/Core/AudioCommon/CubebStream.cpp +++ b/Source/Core/AudioCommon/CubebStream.cpp @@ -12,7 +12,7 @@ #include "Core/System.h" #ifdef _WIN32 -#include +#include #endif // ~10 ms - needs to be at least 240 for surround diff --git a/Source/Core/AudioCommon/CubebUtils.cpp b/Source/Core/AudioCommon/CubebUtils.cpp index 85fb44767e83..0be4fdeb818b 100644 --- a/Source/Core/AudioCommon/CubebUtils.cpp +++ b/Source/Core/AudioCommon/CubebUtils.cpp @@ -14,7 +14,7 @@ #include #ifdef _WIN32 -#include +#include #endif static void LogCallback(const char* format, ...) diff --git a/Source/Core/AudioCommon/WASAPIStream.cpp b/Source/Core/AudioCommon/WASAPIStream.cpp index 07a399a8906d..174ffbc9cb71 100644 --- a/Source/Core/AudioCommon/WASAPIStream.cpp +++ b/Source/Core/AudioCommon/WASAPIStream.cpp @@ -7,7 +7,7 @@ // clang-format off #include -#include +#include #include #include #include diff --git a/Source/Core/AudioCommon/WASAPIStream.h b/Source/Core/AudioCommon/WASAPIStream.h index dfe0974a8cc5..ba058c78d361 100644 --- a/Source/Core/AudioCommon/WASAPIStream.h +++ b/Source/Core/AudioCommon/WASAPIStream.h @@ -6,7 +6,7 @@ #ifdef _WIN32 // clang-format off -#include +#include #include #include #include diff --git a/Source/Core/Common/Arm64Emitter.cpp b/Source/Core/Common/Arm64Emitter.cpp index cdb0b7e625df..51c9823d170d 100644 --- a/Source/Core/Common/Arm64Emitter.cpp +++ b/Source/Core/Common/Arm64Emitter.cpp @@ -21,7 +21,7 @@ #include "Common/SmallVector.h" #ifdef _WIN32 -#include +#include #endif #ifdef __APPLE__ #include diff --git a/Source/Core/Common/ArmCPUDetect.cpp b/Source/Core/Common/ArmCPUDetect.cpp index 68a29caa45e7..91b66f807fdc 100644 --- a/Source/Core/Common/ArmCPUDetect.cpp +++ b/Source/Core/Common/ArmCPUDetect.cpp @@ -12,7 +12,7 @@ #ifdef __APPLE__ #include #elif defined(_WIN32) -#include +#include #include #include "Common/WindowsRegistry.h" #elif defined(__linux__) diff --git a/Source/Core/Common/CommonFuncs.cpp b/Source/Core/Common/CommonFuncs.cpp index 2c70ced6ca87..369d09066ec8 100644 --- a/Source/Core/Common/CommonFuncs.cpp +++ b/Source/Core/Common/CommonFuncs.cpp @@ -9,7 +9,7 @@ #ifdef _WIN32 #include -#include +#include #define strerror_r(err, buf, len) strerror_s(buf, len, err) diff --git a/Source/Core/Common/CompatPatches.cpp b/Source/Core/Common/CompatPatches.cpp index 8697357883e5..91394e96c0b9 100644 --- a/Source/Core/Common/CompatPatches.cpp +++ b/Source/Core/Common/CompatPatches.cpp @@ -1,7 +1,7 @@ // Copyright 2008 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include +#include #include #include #include diff --git a/Source/Core/Common/DynamicLibrary.cpp b/Source/Core/Common/DynamicLibrary.cpp index e9686859d377..ca6c17be6dd9 100644 --- a/Source/Core/Common/DynamicLibrary.cpp +++ b/Source/Core/Common/DynamicLibrary.cpp @@ -8,7 +8,7 @@ #include #ifdef _WIN32 -#include +#include #else #include #endif diff --git a/Source/Core/Common/FileSearch.cpp b/Source/Core/Common/FileSearch.cpp index a90b385ad93e..9aed3373ece2 100644 --- a/Source/Core/Common/FileSearch.cpp +++ b/Source/Core/Common/FileSearch.cpp @@ -11,8 +11,8 @@ #include "Common/Logging/Log.h" #include "Common/StringUtil.h" -#ifdef _MSC_VER -#include +#ifdef _WIN32 +#include #elifdef ANDROID #include "jni/AndroidCommon/AndroidCommon.h" #endif diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 9f17a5867522..10032a03c592 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -29,13 +29,13 @@ #include "Common/StringUtil.h" #ifdef _WIN32 -#include -#include +#include #include // for GetSaveFileName #include // getcwd #include #include // guid stuff #include +#include #else #include #include diff --git a/Source/Core/Common/LdrWatcher.cpp b/Source/Core/Common/LdrWatcher.cpp index a0db0b61c639..31a8278bba70 100644 --- a/Source/Core/Common/LdrWatcher.cpp +++ b/Source/Core/Common/LdrWatcher.cpp @@ -3,9 +3,9 @@ #include "Common/LdrWatcher.h" -#include -#include +#include #include +#include #include typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA diff --git a/Source/Core/Common/Network.cpp b/Source/Core/Common/Network.cpp index 58994c635d05..90dcd7017f19 100644 --- a/Source/Core/Common/Network.cpp +++ b/Source/Core/Common/Network.cpp @@ -13,7 +13,7 @@ #include #include #else -#include +#include #endif #include diff --git a/Source/Core/Common/SocketContext.h b/Source/Core/Common/SocketContext.h index 0aa4929e89cb..5dcf45df5d17 100644 --- a/Source/Core/Common/SocketContext.h +++ b/Source/Core/Common/SocketContext.h @@ -4,8 +4,8 @@ #pragma once #ifdef _WIN32 -#include #include +#include #endif namespace Common diff --git a/Source/Core/Common/StringUtil.cpp b/Source/Core/Common/StringUtil.cpp index e84fabcbb583..6a498474e104 100644 --- a/Source/Core/Common/StringUtil.cpp +++ b/Source/Core/Common/StringUtil.cpp @@ -25,7 +25,7 @@ #include "Common/Logging/Log.h" #ifdef _WIN32 -#include +#include #include constexpr u32 CODEPAGE_SHIFT_JIS = 932; constexpr u32 CODEPAGE_WINDOWS_1252 = 1252; diff --git a/Source/Core/Common/Thread.cpp b/Source/Core/Common/Thread.cpp index 258559bc4f7a..d9a40fc6b592 100644 --- a/Source/Core/Common/Thread.cpp +++ b/Source/Core/Common/Thread.cpp @@ -4,7 +4,7 @@ #include "Common/Thread.h" #ifdef _WIN32 -#include +#include #include #else #include diff --git a/Source/Core/Common/Timer.cpp b/Source/Core/Common/Timer.cpp index 957073426b26..fcbbd7bbd200 100644 --- a/Source/Core/Common/Timer.cpp +++ b/Source/Core/Common/Timer.cpp @@ -8,7 +8,7 @@ #include "Common/CommonFuncs.h" #ifdef _WIN32 -#include +#include #include #endif diff --git a/Source/Core/Common/WindowsDevice.cpp b/Source/Core/Common/WindowsDevice.cpp index 34a540ed7bb7..dc7d4ad68c1e 100644 --- a/Source/Core/Common/WindowsDevice.cpp +++ b/Source/Core/Common/WindowsDevice.cpp @@ -7,7 +7,7 @@ #include -#include "Hidclass.h" +#include "hidclass.h" #include "Common/CommonFuncs.h" #include "Common/Logging/Log.h" diff --git a/Source/Core/Common/WindowsDevice.h b/Source/Core/Common/WindowsDevice.h index 78e8234c15a8..8de1b6b01fbe 100644 --- a/Source/Core/Common/WindowsDevice.h +++ b/Source/Core/Common/WindowsDevice.h @@ -16,11 +16,10 @@ #ifndef NOMINMAX #define NOMINMAX #endif -#include - -#include +#include #include #include +#include #include "Common/Functional.h" diff --git a/Source/Core/Common/WindowsRegistry.cpp b/Source/Core/Common/WindowsRegistry.cpp index 60674206698a..e95bd2da0ca9 100644 --- a/Source/Core/Common/WindowsRegistry.cpp +++ b/Source/Core/Common/WindowsRegistry.cpp @@ -1,6 +1,6 @@ #include "Common/WindowsRegistry.h" -#include +#include #include #include #include "Common/StringUtil.h" diff --git a/Source/Core/Common/WindowsRegistry.h b/Source/Core/Common/WindowsRegistry.h index 7dc3a52f914c..affeba41cb6d 100644 --- a/Source/Core/Common/WindowsRegistry.h +++ b/Source/Core/Common/WindowsRegistry.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include "Common/CommonTypes.h" diff --git a/Source/Core/Core/Config/DefaultLocale.cpp b/Source/Core/Core/Config/DefaultLocale.cpp index 79c4c677872c..55f304c88fd2 100644 --- a/Source/Core/Core/Config/DefaultLocale.cpp +++ b/Source/Core/Core/Config/DefaultLocale.cpp @@ -11,7 +11,7 @@ #include #ifdef _WIN32 -#include +#include #endif #include "Common/Assert.h" diff --git a/Source/Core/Core/DolphinAnalytics.cpp b/Source/Core/Core/DolphinAnalytics.cpp index 936cd7191b0b..1fff6bc5c508 100644 --- a/Source/Core/Core/DolphinAnalytics.cpp +++ b/Source/Core/Core/DolphinAnalytics.cpp @@ -13,7 +13,7 @@ #include #if defined(_WIN32) -#include +#include #include "Common/WindowsRegistry.h" #endif diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/ROM.cpp b/Source/Core/Core/HW/DSPHLE/UCodes/ROM.cpp index 61c55857b218..a2151f949bde 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/ROM.cpp +++ b/Source/Core/Core/HW/DSPHLE/UCodes/ROM.cpp @@ -4,7 +4,7 @@ #include "Core/HW/DSPHLE/UCodes/ROM.h" #ifdef _WIN32 -#include +#include #endif #include "Common/ChunkFile.h" diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/UCodes.cpp b/Source/Core/Core/HW/DSPHLE/UCodes/UCodes.cpp index d6fd59b14504..5bc1e2b1d04c 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/UCodes.cpp +++ b/Source/Core/Core/HW/DSPHLE/UCodes/UCodes.cpp @@ -7,7 +7,7 @@ #include #ifdef _WIN32 -#include +#include #endif #include "Common/ChunkFile.h" diff --git a/Source/Core/Core/HW/EXI/BBA/BuiltIn.h b/Source/Core/Core/HW/EXI/BBA/BuiltIn.h index 691431f1c850..d1f656de6465 100644 --- a/Source/Core/Core/HW/EXI/BBA/BuiltIn.h +++ b/Source/Core/Core/HW/EXI/BBA/BuiltIn.h @@ -4,7 +4,7 @@ #pragma once #ifdef _WIN32 -#include +#include using socklen_t = int; #else #include diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h index 00d0fcb84eea..59fabfafd11f 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h @@ -11,7 +11,7 @@ #include "SFML/Network/IpAddress.hpp" #ifdef _WIN32 -#include +#include #endif #include diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp index 2d3d66feabd0..714fdbf87063 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceMic.cpp @@ -20,7 +20,7 @@ #include "Core/System.h" #ifdef _WIN32 -#include +#include #endif namespace ExpansionInterface diff --git a/Source/Core/Core/HW/WiimoteReal/IOWin.cpp b/Source/Core/Core/HW/WiimoteReal/IOWin.cpp index b65bab19b503..b2e4a5cab030 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOWin.cpp +++ b/Source/Core/Core/HW/WiimoteReal/IOWin.cpp @@ -9,11 +9,11 @@ #include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include // initguid.h must be included before devpkey.h diff --git a/Source/Core/Core/IOS/FS/FileSystem.h b/Source/Core/Core/IOS/FS/FileSystem.h index 8d9f5ebf649a..cf9bf8c64b25 100644 --- a/Source/Core/Core/IOS/FS/FileSystem.h +++ b/Source/Core/Core/IOS/FS/FileSystem.h @@ -12,7 +12,7 @@ #ifdef _WIN32 // TODO: Horrible hack, remove ASAP! -#include +#include #endif #include "Common/CommonTypes.h" diff --git a/Source/Core/Core/IOS/Network/ICMP.h b/Source/Core/Core/IOS/Network/ICMP.h index 258a9b6c3f05..64131f02d334 100644 --- a/Source/Core/Core/IOS/Network/ICMP.h +++ b/Source/Core/Core/IOS/Network/ICMP.h @@ -4,7 +4,7 @@ #pragma once #ifdef _WIN32 -#include +#include #else #include #endif diff --git a/Source/Core/Core/IOS/Network/Socket.h b/Source/Core/Core/IOS/Network/Socket.h index 4f07612799b2..88a018dc9c9e 100644 --- a/Source/Core/Core/IOS/Network/Socket.h +++ b/Source/Core/Core/IOS/Network/Socket.h @@ -4,8 +4,8 @@ #pragma once #ifdef _WIN32 -#include #include +#include #include typedef pollfd pollfd_t; diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index db261eb0764b..fe0363c915f9 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -21,7 +21,7 @@ #include "Core/System.h" #ifdef _WIN32 -#include +#include #endif #ifdef ANDROID diff --git a/Source/Core/Core/NetworkCaptureLogger.h b/Source/Core/Core/NetworkCaptureLogger.h index 27489ec089ce..1324171eef8e 100644 --- a/Source/Core/Core/NetworkCaptureLogger.h +++ b/Source/Core/Core/NetworkCaptureLogger.h @@ -9,7 +9,7 @@ #include #ifdef _WIN32 -#include +#include using socklen_t = int; #else #include diff --git a/Source/Core/Core/PowerPC/GDBStub.cpp b/Source/Core/Core/PowerPC/GDBStub.cpp index 088a74c92635..f8e6c51a1455 100644 --- a/Source/Core/Core/PowerPC/GDBStub.cpp +++ b/Source/Core/Core/PowerPC/GDBStub.cpp @@ -9,8 +9,8 @@ #include #include #ifdef _WIN32 -#include #include +#include #include typedef SSIZE_T ssize_t; #define SHUT_RDWR SD_BOTH diff --git a/Source/Core/DolphinNoGUI/MainNoGUI.cpp b/Source/Core/DolphinNoGUI/MainNoGUI.cpp index 1e1ff6e39fa9..3bf40ea0d42b 100644 --- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp +++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp @@ -12,7 +12,7 @@ #ifndef _WIN32 #include #else -#include +#include #endif #include "Common/ScopeGuard.h" diff --git a/Source/Core/DolphinNoGUI/PlatformWin32.cpp b/Source/Core/DolphinNoGUI/PlatformWin32.cpp index ff0769811526..90d337200c38 100644 --- a/Source/Core/DolphinNoGUI/PlatformWin32.cpp +++ b/Source/Core/DolphinNoGUI/PlatformWin32.cpp @@ -8,7 +8,7 @@ #include "Core/Core.h" #include "Core/System.h" -#include +#include #include #include diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index b223819e7a35..6ebff534625b 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #endif #ifdef __linux__ diff --git a/Source/Core/DolphinQt/RenderWidget.cpp b/Source/Core/DolphinQt/RenderWidget.cpp index 9b5026d5fb71..49680b848230 100644 --- a/Source/Core/DolphinQt/RenderWidget.cpp +++ b/Source/Core/DolphinQt/RenderWidget.cpp @@ -32,7 +32,7 @@ #include "VideoCommon/Present.h" #ifdef _WIN32 -#include +#include #include #endif diff --git a/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.cpp b/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.cpp index 4379749e5f8e..3d3d8049c05b 100644 --- a/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.cpp +++ b/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.cpp @@ -7,8 +7,8 @@ #include #include -#include -#include +#include +#include namespace ciface::DInput { diff --git a/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.h b/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.h index de3d68437258..52911dabaa13 100644 --- a/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.h +++ b/Source/Core/InputCommon/ControllerInterface/DInput/XInputFilter.h @@ -3,7 +3,7 @@ #pragma once -#include +#include #include namespace ciface::DInput diff --git a/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h b/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h index 6a00d2812aa1..dabc1d5c63e0 100644 --- a/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include "Common/Event.h" #include "Common/Flag.h" diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp index f6912f67ced3..5aeb9e07db9a 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp @@ -8,7 +8,7 @@ #include #ifdef _WIN32 -#include +#include #endif #include diff --git a/Source/Core/InputCommon/ControllerInterface/Win32/Win32.cpp b/Source/Core/InputCommon/ControllerInterface/Win32/Win32.cpp index 79584913cf77..eb4bd6fd6bbe 100644 --- a/Source/Core/InputCommon/ControllerInterface/Win32/Win32.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Win32/Win32.cpp @@ -3,7 +3,7 @@ #include "InputCommon/ControllerInterface/Win32/Win32.h" -#include +#include #include // must be before hidclass #include diff --git a/Source/Core/InputCommon/ControllerInterface/XInput/XInput.h b/Source/Core/InputCommon/ControllerInterface/XInput/XInput.h index 95fe6a3c6f98..b006d1c7cc55 100644 --- a/Source/Core/InputCommon/ControllerInterface/XInput/XInput.h +++ b/Source/Core/InputCommon/ControllerInterface/XInput/XInput.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include "InputCommon/ControllerInterface/ControllerInterface.h" diff --git a/Source/Core/UICommon/AutoUpdate.cpp b/Source/Core/UICommon/AutoUpdate.cpp index 4298382b32fc..f90f4d5ff852 100644 --- a/Source/Core/UICommon/AutoUpdate.cpp +++ b/Source/Core/UICommon/AutoUpdate.cpp @@ -18,7 +18,7 @@ #include "Common/Version.h" #ifdef _WIN32 -#include +#include #else #include #endif diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.cpp b/Source/Core/UpdaterCommon/UpdaterCommon.cpp index 0d98f191878d..8a400b21d05c 100644 --- a/Source/Core/UpdaterCommon/UpdaterCommon.cpp +++ b/Source/Core/UpdaterCommon/UpdaterCommon.cpp @@ -32,7 +32,7 @@ #endif #ifdef _WIN32 -#include +#include #include #endif diff --git a/Source/Core/WinUpdater/Platform.cpp b/Source/Core/WinUpdater/Platform.cpp index c48271be55b4..8e6d0eaf6f31 100644 --- a/Source/Core/WinUpdater/Platform.cpp +++ b/Source/Core/WinUpdater/Platform.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/Source/Core/WinUpdater/WinUI.cpp b/Source/Core/WinUpdater/WinUI.cpp index c6e65626e943..0a9217291123 100644 --- a/Source/Core/WinUpdater/WinUI.cpp +++ b/Source/Core/WinUpdater/WinUI.cpp @@ -7,11 +7,11 @@ #include #include -#include -#include -#include -#include +#include +#include #include +#include +#include #include #include "Common/Event.h" diff --git a/Source/PCH/pch.h b/Source/PCH/pch.h index 484f2d343b9f..e1322885480e 100644 --- a/Source/PCH/pch.h +++ b/Source/PCH/pch.h @@ -94,7 +94,7 @@ #include #ifdef _WIN32 -#include +#include #endif #include "Common/Common.h" From a5f6ba81fad6fc5378e1a817b776b70754083551 Mon Sep 17 00:00:00 2001 From: Martino Fontana Date: Tue, 16 Jun 2026 22:27:02 +0200 Subject: [PATCH 110/270] GameSettings: Approve speed hacks for RetroAchievements Some of the speed hacks were not approved for RetroAchievements, meaning that users had to choose between not getting hardcore achievements, or having poor performance. Also, renamed the instances of `$Limit internal FPS to VPS (improves performance)` to `$Limit internal frame rate (speed hack)` for consistency. They are enabled by default, so unless the user disabled them, it won't break the configuration. --- Data/Sys/ApprovedInis.json | 2 +- Data/Sys/GameSettings/GDSE78.ini | 3 +++ Data/Sys/GameSettings/GDSP78.ini | 3 +++ Data/Sys/GameSettings/GE4.ini | 7 +++++-- Data/Sys/GameSettings/GH2E69.ini | 3 +++ Data/Sys/GameSettings/GH2P69.ini | 3 +++ Data/Sys/GameSettings/GHQP7D.ini | 3 +++ Data/Sys/GameSettings/GMNE78.ini | 6 +++--- Data/Sys/GameSettings/GMNP78.ini | 6 +++--- Data/Sys/GameSettings/GTRE78r0.ini | 6 +++--- Data/Sys/GameSettings/GTRE78r1.ini | 6 +++--- Data/Sys/GameSettings/GTRJ8N.ini | 6 +++--- Data/Sys/GameSettings/GTRP78.ini | 6 +++--- Data/Sys/GameSettings/R7XE69.ini | 6 +++--- Data/Sys/GameSettings/R7XJ13.ini | 6 +++--- Data/Sys/GameSettings/R7XP69.ini | 6 +++--- Data/Sys/GameSettings/RGWE41.ini | 3 +++ Data/Sys/GameSettings/RGWP41.ini | 3 +++ Data/Sys/GameSettings/RGWX41r0.ini | 3 +++ Data/Sys/GameSettings/RGWX41r1.ini | 3 +++ Data/Sys/GameSettings/RJ2E52.ini | 3 +++ Data/Sys/GameSettings/RJ2JGD.ini | 3 +++ Data/Sys/GameSettings/RJ2P52.ini | 3 +++ Data/Sys/GameSettings/RQZ.ini | 3 +++ Data/Sys/GameSettings/SCYE4Q.ini | 3 +++ Data/Sys/GameSettings/SCYP4Q.ini | 3 +++ Data/Sys/GameSettings/SCYR4Q.ini | 3 +++ Data/Sys/GameSettings/SCYX4Q.ini | 3 +++ Data/Sys/GameSettings/SCYY4Q.ini | 3 +++ Data/Sys/GameSettings/SCYZ4Q.ini | 3 +++ Data/Sys/GameSettings/SDVE41.ini | 3 +++ Data/Sys/GameSettings/SDVP41.ini | 3 +++ Data/Sys/GameSettings/SQIE4Q.ini | 3 +++ Data/Sys/GameSettings/SQIP4Q.ini | 3 +++ Data/Sys/GameSettings/SQIY4Q.ini | 3 +++ Data/Sys/GameSettings/STSE4Q.ini | 3 +++ Data/Sys/GameSettings/STSP4Qr1.ini | 3 +++ Data/Sys/GameSettings/STSP4Qr2.ini | 3 +++ Data/Sys/GameSettings/STSX4Q.ini | 3 +++ Data/Sys/GameSettings/STSY4Qr0.ini | 3 +++ Data/Sys/GameSettings/STSY4Qr1.ini | 3 +++ Data/Sys/GameSettings/STSZ4Q.ini | 3 +++ Data/Sys/GameSettings/WR2E41.ini | 3 +++ Data/Sys/GameSettings/WR2P41.ini | 3 +++ Source/Core/Core/AchievementApprovedHash.h | 4 ++-- 45 files changed, 134 insertions(+), 32 deletions(-) diff --git a/Data/Sys/ApprovedInis.json b/Data/Sys/ApprovedInis.json index 2642012deda7..de9bf9acff6b 100644 --- a/Data/Sys/ApprovedInis.json +++ b/Data/Sys/ApprovedInis.json @@ -1 +1 @@ -{"D43J01":{"loophack":"CAB9CED2D904F12CCB21F5B1DE9B5433620C3E13"},"DPOJ8P":{"Bypass Modem Detection":"90BA66E25640A538FEFA6693715718139079FA7B"},"G2BE5G":{"Disable interlaced rendering":"7FFF6BDD93713BEDFD23739C32B86153FA19AEA0"},"G2BP7D":{"Disable interlaced rendering":"56E85D7285F10348E1E5354E379918D07E79EDA9"},"G2VE08":{"16:9 Widescreen":"3ED033396382424533A263A39687A0961EC994A6"},"G2VP08":{"16:9 Widescreen":"EEBC3C5F04BB70AFCA1A4C3D638335CDC9142BA2"},"G3RP52":{"16:9 Widescreen":"7F6B7743F0DDD3704B15882330E490103AE24E36","60fps":"4C9A8572A35F1DB85BBB2F32ED34288A343F1FC0"},"G3YP52":{"16:9 Widescreen":"3D102AFEE2F4945894E74BB702ED3ED2294C6FEC","Force PAL60":"FD61581C593EA34DD358AE33819FAB310F2CE961"},"G4AEE9":{"16:9 Aspect Ratio Fix":"8C2564CF47BB6A9B57D712C90E17F459B0BB6CCD"},"G4ME69":{"16:9 Widescreen":"6FC6E5D91693FAABE25CC721F2121CB0B5CA9D12"},"G5SE7D":{"16:9 Widescreen":"16119398BFE241A84151C046E0BC5BCF89D22049","Remove Bloom":"A5F2FB2B3BFF61F34F8AB1DD5A912A0FD8DFA720"},"G5SP7D":{"16:9 Widescreen":"718AA8BD831961C33114852FDA75BB58F768F46A"},"G8ME01":{"16:9 Aspect Ratio Fix - Centered HUD":"8C2FC3FC6F7766AA8E87425BF3BDD2E46E73DE71","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"AE3DBE2604CA0660165CA870C4B80E2B523C9475","16:9 Aspect Ratio Fix - Normal HUD":"F6E8264CF14D0189219C1E2D20C4182F190F4B88","16:9 Aspect Ratio Fix - Stretched HUD":"8EA4C7B80ADE3E8FEC28EEFA3A046B6B04162E81","No Letterbox":"B1DE2A3526F2E4511F2B94A5171BF7B3C2C52B9B"},"G8MJ01":{"16:9 Aspect Ratio Fix - Centered HUD":"B3181CA1AAB11EAE9132184DA48D76E775AC0A24","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"904C0EEEF16D761F7BDF7099DEB384DD9AB26E00","16:9 Aspect Ratio Fix - Normal HUD":"631CE99BDE3F1475303C00C67E3C130D4EB2547B","16:9 Aspect Ratio Fix - Stretched HUD":"81F7AD591F12A01DE82850457ECF147998D1238F"},"G8MP01":{"16:9 Aspect Ratio Fix - Centered HUD":"75AF5D7FA49BE9C3C73EA1482B49CD9B36CE817E","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"C2E5003E74551DF693A7CB814850CA51D77549EF","16:9 Aspect Ratio Fix - Normal HUD":"FB26AA84B0CB787705E84E869B649307776A2839","16:9 Aspect Ratio Fix - Stretched HUD":"C5F8A81520193418C07C7BC4881F02BE9C4DC9BC","No Letterbox":"7828F5B0C8B470620755C16C60A5A4554F92640C"},"G9SE8P":{"16:9 Widescreen":"CD043229AC47AC93D537B6A0725A2D0BB6FEF4E8"},"G9SJ8P":{"16:9 Widescreen":"C55EF90B542AA3973352A232629A828BC22A8509"},"G9SP8P":{"16:9 Widescreen":"9521774ADEE769CFCCE86F8170FE626387E73019"},"GAFE01":{"16:9 Widescreen":"D24F6D9BE5D79D3C795C2844634A0214BB68B5DA","Make Game Save Copyable (donny2112)":"F9A812FF62A20D5440CAB79DB6AFAE068457A10E"},"GAHEGG":{"Limit internal frame rate (speed hack)":"2873E5178C8068AFA8AFE751F903A69CA9A41F16"},"GAL":{"Trophy Viewer":"DBDB50216B22CDFFF77BDD80261CD0FF75803D3B"},"GALE01r0":{"Normal C Stick Functionality in Singleplayer Modes":"9DC8D90D986FAC175F45006AFF20CF00EDFBE053","Proper 16:9 Widescreen Support":"18B4159E77013EDFE499A33281B771C442970E3B","Properly Display in 4:3":"DC2BCA7D99EBDE1DC7C68CA6AC7F543B93FD6A67"},"GALE01r1":{"Normal C Stick Functionality in Singleplayer Modes":"F6030959C1CAC92F37287D556F18B3C5298FAD5B"},"GALE01r2":{"Disable Rumble":"CFDEEA8AD2A99394BFFEA7211EF03394E7471FA9","Normal C Stick Functionality in Singleplayer Modes":"BCD918A72368B2A30E797C76994E7539A1C0B5D9","PAL Stock Icons":"6B92D95F0EEA165EA84FD753F74B0A9E2079C2D7","Proper 16:9 Widescreen Support":"CEDF18354CDCFC49398551C629E51B053331A55B","Widescreen Support v1.2 [Dan Salvato]":"D32ED4CD685F3FF649F7E749E8C182C657013918"},"GALP01":{"Normal C Stick Functionality in Singleplayer Modes":"4817F202678559291F9F76B46DC7561A45D00B77","Proper 16:9 Widescreen Support":"BFCBE098FE8679B9140A48EA1D8620D5A369697C","Properly Display in 4:3":"5BA2933B8365E21553AA273B91954A4C722B1035"},"GAZE69":{"60FPS":"BC281053C5266B21FDDC6C8996A6021DEA03D459"},"GBLE52":{"16:9 Widescreen":"C0F3AB3BBC9B21F4BF25F44E48CF1CD36D2B3F45"},"GBLP52":{"16:9 Widescreen":"797040CB097BFD369490A1BE29659929D76CE6F7"},"GBRJ18":{"16:9 Widescreen":"B3C867AB34D90E2E9C5B799D800F4C191DE279FA"},"GBSE8P":{"16:9 Widescreen":"4148D453F61A5B81E53669335BC667E651865C76"},"GBSP8P":{"16:9 Widescreen":"6CB029AE768BA5B9995D5F12C1EBB37B3D1FE242"},"GC6E01":{"16:9 Widescreen":"DE932CCAD1B5EB1FAA8D08733BD0D3940C78382A","60 FPS [Nerdzilla]":"85F98064F17AFE0FB02A99A182AF3FEB7644DCAA","Allow Memory Card saving with Savestates":"2F64F98686E62B60E466E931A9EBCD19A750FF4E"},"GC6J01":{"60 FPS [Nerdzilla]":"A6146772E37970EA5564F51A622A04792158B321","Allow Memory Card saving with Savestates":"D8F327304A88FBC717BB1F775494C5F864B9E8D2"},"GC6P01":{"16:9 Widescreen [Ralf]":"05C9016BF58A628A3FD7B16E1B9BB0769A14846F","60 FPS [Nerdzilla]":"1D3FB017C0D20754E48A8BA54EEE7DC4851BE84B","Allow Memory Card saving with Savestates":"EDEE0E28EEA1834868F2865336290FFBDFB9C6DA"},"GC8JA4":{"Fix C4 texture tiling (used for buttons and some character icons)":"843297019804192AF1FC660BC85F4B10891D0BA3"},"GCBE7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD"},"GCBP7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD","60Hz":"DF2C4DDF082C9E67919443C5ACC485AB2CF7F8D0"},"GCCE01":{"16:9 Widescreen":"BA0066518E7EA8599A81F8496BD39DE20CC369D6","Fix GBA connections":"483BDB94615C690045C3759795AF13CE76552286","Fix buffer overrun bug (crash at Goblin Wall)":"6C107FEC15C76201233CA2645EB5FAB4FF9751CE"},"GCCJGC":{"Fix GBA connections":"4C104D24329172F5D0F8649DE9423B931FE72CA3"},"GCCP01":{"16:9 Widescreen":"2580E7379633CF4DE13B9EC945DB17543477828A","Fix GBA connections":"2EAA60A8A115AD68A795109FB59E4A726D29016D"},"GCN":{"Fix C4 texture tiling (used for buttons and some character icons)":"601FE183C9524ACCF068874DABD73921C86769CF"},"GCVEEB":{"16:9 Widescreen":"ADA686C90189D1D7D23E23E525EFC96EFA104BB9"},"GD7E70":{"Deinterlacing Fix":"6F8B6E33214162E5CDEBD2BC6A60FAFA04D2EB10"},"GD7PB2":{"Deinterlacing Fix":"3B798C9D63E755349B253FAF9186BAE1F703D96A"},"GDREAF":{"Fix audio issues":"F8EAE60FEB0CFB4477FDC4B9E136B63F68DFA63A"},"GDRP69":{"Fix audio issues":"E23D98B2CE185C3993A40F2495D37E41B971BF91"},"GEDE01":{"Fix startup hang":"21068C3CE905FB0CFFAA7408A93154AF8A5295A2"},"GEDJ01":{"Fix startup hang":"7061F3CF11BF64D3BA7F32CCF2BAC42FF3614AB6"},"GEDP01":{"Fix startup hang":"6F1B00517CBA30BEB738EAA90E71221378CD570D"},"GEME7F":{"Force Progressive Scan":"CB04E00918C9C0F161715D21D046ED6620F7ADEF"},"GEMJ28":{"Force Progressive Scan":"41D8F2C833C0B7B79FCA038752052059207AE4D5"},"GEZE8P":{"16:9 Widescreen [gamemasterplc]":"5CEBCFBEA4E444495D2FD6D8B6607DFB2349CC1B"},"GF7E01":{"16:9 Widescreen [gamemasterplc]":"C59CCA3EF8A5E97B32EB64DB9AE80E652ED281C6"},"GF7P01":{"16:9 Widescreen [gamemasterplc]":"1230053B5C347B62E432EFD6635433A183D18619"},"GFQEA4":{"16:9 Widescreen":"5CD9568CE58EF82EB371594667015C9B799454B9"},"GFZJ01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GFZP01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GGTE01":{"16:9 Widescreen [darkludx]":"EF128E7A9C22676834F558BA0F0F7FD8B9028727","Experimental 60fps":"D40344111D989EA009901F8B1C45B5AC8D39E6D2","Simple 60FPS":"5232A937D1D813FF58DD71D716284ED6AB535689"},"GGTJ01":{"Experimental 60fps":"BEC2310911003AF9E4B984A4051E0F885B8CC825","Simple 60FPS":"D62E777A2F019D7BD0AFFBCE876BD9AE408F1667"},"GGTP01":{"16:9 Widescreen":"FA9CD330ECDA01275DA88BD0803DE47757D16A4E","Experimental 60fps":"EB1ACD613BB432A3391CFFF1F0145A6ACE66B210","Simple 60FPS":"43FAD0DDD130BE1E4C8C7603EF6CA7DAA0DF5EE9"},"GGVD78":{"16:9 Widescreen":"9D07DBB5EC2FAA47F2E4587FBD75FD6F3E7E91B0","EFB Copy Fix":"FE52240DF6D132C15A8324E8A477F2BF2250D208"},"GGVE78":{"16:9 Widescreen":"86E561452235BF88D41884558EF34F54CE0FEB48","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVP78":{"16:9 Widescreen":"F9B2108D833084FA36A53E00F1647A9579F847D3","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVX78":{"EFB Copy Fix":"740F2D1C01DA39D1760D96B03974A48E6F74578D"},"GHAE08":{"Fix audio issues":"9799AFF8463EC86C9230E31E2627E141F0C129D3"},"GHAJ08":{"Fix audio issues":"B45A8FC32D14567B8D6C95F303E00A72C0E1D344"},"GHAP08":{"Fix audio issues":"BC7F3CFC97593AA2055C370C175950DC478D2709"},"GHKD7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKE7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"9BFBDE9184BC05EBD545625DEE486E3AAB36B6A3"},"GHKF7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHKP7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKS7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHLE69":{"16:9 Widescreen":"742BFC79D8F0BA5D6215772B58F5A0EADD7BFAFF"},"GHQE7D":{"16:9 Widescreen":"520E5F718D3BD1291C55021776091E0DE0FB4822","Limit internal frame rate during startup (speed hack)":"F6BF450D22104FD2527122EA6E81B1F70E916B95"},"GHQP7D":{"16:9 Widescreen":"AAE14CDC13A0C082A850DED85417100A8C8A0687","60Hz":"884A33613AE8D916128E3FF39B3AA9F63DAADC58","Limit internal frame rate during startup (speed hack)":"BD475ADEF145AC9226514CD1B2D4D5F3BFE67964"},"GHSE69":{"16:9 Widescreen":"3538CDBD13FA939B7BA5F0F82253401AFDD1E2CD"},"GHSP69":{"16:9 Widescreen":"409754E2EBB6F05DEE1AFC647E25B15D462638FF"},"GICD78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICE78":{"EFB Copy Fix":"5BF55685B8867A85EAA9C86571309B17BF7DED32"},"GICF78":{"EFB Copy Fix":"85AABAEB9A59C4F96D9330A3B884F6D757DA1683"},"GICH78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICJG9":{"EFB Copy Fix":"969134EA21A160EBDA91C0870266E7D1707FDC43"},"GICP78":{"EFB Copy Fix":"13B158CF41F5412BC637F50644193D43CC3DA49A"},"GIQE78":{"EFB Copy Fix":"E15AA1E30D26E5735D68AAADE436E7B7E4A33A35"},"GIQJ8P":{"EFB Copy Fix":"FFFCB76E98DDB06A7BBBC0AA73C869C87EB787D6"},"GIQX78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GIQY78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GK2":{"16:9 Widescreen [Vague Rant]":"6B4C59C2A2C5D71C7A38C8513A76E6467E307A26"},"GK2E52":{"60 FPS":"8F2397930A50C3C176188526EB9612578F9CBCE5"},"GK4":{"16:9 Widescreen":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKB":{"16:9 Widescreen Region-Free":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKBPAF":{"60Hz":"8E4E0ABA0E6D102A33206F34F4DEE63159B3CB4F"},"GKDP01":{"16:9 Widescreen":"DF4657937DE730107B636C73E959277EB963D210","60Hz":"6740D7B914CC897AF50798C0AB391965294FD1E1"},"GKRPB2":{"16:9 Widescreen":"69C01A4F91D991CF866237FED94A138308FC104F","60Hz":"40D06A5C7A3D873ABC73DEDF573738C2E3E37FF0"},"GKWJ18":{"16:9 Widescreen":"696570101FA040778EAD310377C484C846D87430"},"GKYE01":{"16:9 Widescreen":"6520EF1B7D88F00747B120A3A010458602ED989D"},"GKYJ01":{"16:9 Widescreen":"47B8EF7D02831AA5C375C698797BF2D5475FEFF2"},"GKYP01":{"16:9 Widescreen":"69413C75036D2975716066E6574461B981FF0124"},"GLEE08":{"Fix audio issues":"7355F358CAC6F418D37E4C23E64F7867D46E4FC9"},"GLEJ08":{"Fix audio issues":"12B24A6D7389A2AC5AB75FC0BF8493E7661F2A73"},"GLEP08":{"Fix audio issues":"81BD39F5527552DE89E3B59BA86298900F0A3168"},"GLSD64":{"Fix freeze in opening cutscene":"5E2A73717BD66EF647846DD64C33BC80AD9B5227"},"GLSE64":{"Fix freeze in opening cutscene":"1CE78E7954415A44DF693C0BB879AA5A4FF059A3"},"GLSF64":{"Fix freeze in opening cutscene":"009B0C4AD80A9C28C987934D254C2C4AACC9A07A"},"GLSP64":{"16:9 Widescreen":"8E7A544C10E7A5E2F0304A0D2586879627EF6586","60Hz":"B67144E87B54246137142992A3BC83DC13BE68A7","Fix freeze in opening cutscene":"3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC"},"GM4E01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"78BE50F93E81A1972CA31ABC318949E366E82BD0"},"GM4P01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"6D950B6EEFD6D304E1A424355B74A595D32BDF2F"},"GMBE8P":{"16:9 Widescreen":"D12DE9E3941BCE6EB50B8DA92140E09AFC0104C8","NA Stretched HUD":"0151A6683E1FBFD20096807C54A6952C4A4BAD40"},"GMBP8P":{"16:9 Widescreen":"8A688981F33A2C53882EF08FCF20B88AD43EF417"},"GMNE78":{"Limit internal FPS to VPS (improves performance)":"DE38BC8493EA24F93B6D805F28746AA99C53C677"},"GMNP78":{"Limit internal FPS to VPS (improves performance)":"65FB9B430F7D58EBC76DAADE8CA9DBBAA32B8B51"},"GMOP70":{"16:9 Widescreen":"CE4D298616BD42E4D8F8936B325CD1FD7F5B956B","60Hz":"0CD2CC787A5FF551901E41C85C8AAC02017ECB89"},"GMPE01":{"16:9 Widescreen":"4F98592DB3DEE3857469A8D8605FAF4BD6F7C76D","QOL - Allow Digital Presses for Map Screen":"E232B27564E9AA0C32DE163C9C056317A7B2B12E","QOL - Automatically Advance Text Boxes":"59607671BFC4717ACAF9807BB7EB0D9F982866D4","QOL - Disable Advance on Results":"CAEA37F3FEF89400513353EB85875F2A3AF4C03E","QOL - Faster Boot Time":"F75FBBD838C5B84FF687962FBA9195B217DE132C","QOL - Increased Board Speed":"19F264DE7F07EAC9433CA4B591BEBA1EC976C9F1","QOL - Increased Taunt Capabilities":"C70391D4961A0E820DE40141C89341369A9F021B","QOL - Instant Text Display":"F4E6913CF034E5778B9F9CA5FF448FF1B78B3333","QOL - Rumble Always Off":"93621947019532F02C25937FD3EEEE21A788CB07","QOL - Show Controller Port Number of Who Paused":"D7249AC3C949FEB860CF4350C5B7B79AC16569F1"},"GMPJ01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMPP01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMSE01":{"60FPS":"2805B1A0DD53EB64877D375D10F667700955720F","Widescreen":"BD718F961DBA5372B1D0257D454D535746C453A0"},"GMSJ01":{"16:9 Widescreen":"E8112A01040A06460E368F142C5D1FD0B4085D94"},"GMSJ01r0":{"60FPS":"9894B5B6B5215844D48411021FF8EECFE851D79D"},"GMSJ01r1":{"60FPS":"2DB69DB7A3753D543CD85CA4C77B4EF0AED7486E"},"GMSP01":{"60FPS":"D07009B710B3BBC6B12E54ED6A7969D58197EDAF","Widescreen":"0B7B89BF0868393076EF5F9DBD689DD0EDBCB84C"},"GNHE5d":{"Nop Hack":"89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF"},"GOCE5D":{"16:9 Widescreen":"E4D800B90F16A15D9252EED1B2A23AEAF5CD230A"},"GOME01":{"16:9 Widescreen":"C33CBBF6F22195BF626104B4827200B3EE1CF30C"},"GOMP01":{"16:9 Widescreen":"874784CCD4AC3DC9C69654E471C18AD0F5E4D9AE"},"GONE69":{"Widescreen Hack Culling":"6412A67E1C26E146A9526AFC9F0EE517B214069D"},"GP4J18":{"16:9 Widescreen":"BF4D2D6AF66F285E81173B3B20D5369DB6FB351B"},"GP5E01":{"16:9 Widescreen":"5ED053787332DB5EEB8B963BA5DF197B58C6BB75","QOL - Automatically Advance Text Boxes":"41EF84663008668682BB75DC8B874E38669AE041","QOL - Disable Advance on Results":"6C36BEFE751131298BC8B39012892F6A6903CE60","QOL - Faster Boot Time":"8E952E6E12639AC20F6F9FDD150C12F0C294DF43","QOL - Increased Board Speed":"4BD72B8ED75ED8EFB3329C104BAD114FDDB8AD2C","QOL - Increased Capsule Throwing Speed":"C5BA3C9D386D09DD6CB9242A12A0D041BEB93160","QOL - Increased Taunt Capabilities":"C360EBDDCB0237076052C983BD9719411CA67CDE","QOL - Instant Text Display":"2CD4A06A3B9C18CC9EA2A2DEBD96E066CD1D700A","QOL - Show Controller Port Number of Who Paused":"5BA3648D18346EDC97B23475D4748C637E6095A2"},"GP5J01":{"16:9 Widescreen":"81E69B6BEE85E06805EC24E1CFEA9405BF459DF8"},"GP5P01":{"16:9 Widescreen":"FD4FCCB97C06F0173A30D4CC9A99422B2C0ABA9B"},"GP6E01":{"16:9 Widescreen":"B502AF0887792E9B140516D14BA75F99772C17BE","QOL - Auto Advance Text Boxes":"E645DF70CE0266D174D917A817A7FBEEE974A9F0","QOL - Disable Advance on Results":"798E566B621F6A32BF23C588E76EAC0776E2A8A3","QOL - Faster Boot Time":"B089A3C80D5DA86ABF6258F8BFB1DC78BB54A973","QOL - Increased Board Speed":"501C52ACF117950888A66DB2BEDBDFD7D9B20235","QOL - Increased Orb Throwing Speed":"845F439278DA8CBC225658FF4E2F82D707422BFB","QOL - Increased Taunt Capabilities":"0AED27341A06186AD92D5725C0B9693CA8F85B7B","QOL - Instant Text Display":"5AAC4E21927D4AEE7AC329CE793D4CDAEDDF7A3F","QOL - Show Controller Port Number of Who Paused":"DF5D2F068D76ABF2B1C294D413B9154B8C356929"},"GP6J01":{"16:9 Widescreen":"88F50F8298F82E3E5C161277BD4A985CE893A9D0"},"GP6P01":{"16:9 Widescreen":"DCCE59CA987624C753F0A86BEBA4287635901C93","Remove Black Bars":"138A86D43B5830BFE1926F58C0A4FBE2971BB02D"},"GP7E01":{"16:9 Widescreen":"87EBF72D90503EBC5CE9BC994795669E48A92A4A","QOL - Auto Advance Text Boxes":"82338AB5D82A7F398B4EB674CA4866DD1F4A1E65","QOL - Controller Options Always Acesssible":"D2F9CC0B9E52136E77187395FB7DA9B64B2C3B30","QOL - Disable Advance on Results":"A644551162A61DE7997B2A84F5BFDEFFD3FC2FB5","QOL - Faster Boot Time":"1939B5C575AE8FF6890BDDDD1B3FEA472D8CABA0","QOL - Increased Board Speed":"7E06A2EBCFECADB8074152B489984237A6FE9F21","QOL - Increased Orb Throwing Speed":"6671BEE71920D6B845AE1257AFE3AB3B95CD6FBD","QOL - Increased Taunt Capabilities":"9F60923F95FFE709CCD06966CD2DF743263934D1","QOL - Instant Text Display":"48FA6002E42DA9BB83ABC980A32C60CE3B21C4FC","QOL - Show Controller Port Number of Who Paused":"5E6F8C6C87D5944DB18A4E4F9A45F61EE87A4163"},"GP7J01":{"16:9 Widescreen":"A081F0729068D233E99DC00C64E36CA33C945640"},"GP7P01":{"16:9 Widescreen":"75D6CDA4EA301A71F7701A0487544E79136850B9","60Hz":"6D0D5B7CA58C38A3147F8ED981B5B21CC6C519CD"},"GPIE01r0":{"16:9 Widescreen":"2B58166A66644F984E64077A6245C94C79B51063"},"GPIE01r1":{"16:9 Widescreen":"9A1545AB4E2B9216B95AFFA83420D723512DA8C8","60FPS":"3337C767EAA21D30C6CFEAE9985FFCABFC63E2E3","Disable Pikmin optimizations":"CBD15E9104929F0631713AE482475CB2B90E71F2","No Blur":"A25CDDB79991F090BD7F12840AD06F13BD5CCF95"},"GPIP01":{"16:9 Widescreen":"5AE44D5B0E0D4C412B453B95CF5A41DBA4D685E6","60Hz":"AE59FF751E167632AE283F38CC4B96BDF7B9B81F","Turn off blur":"4ED79A548DBD7DBD35928A2F1138315FF103E260"},"GPNE08":{"16:9 Widescreen [Ralf, darkludx]":"ED3D9BB08C5F1D21BC8D5C73155C00E3C25C466F"},"GPNP08":{"16:9 Widescreen [Ralf]":"469B18FE8219031D355557B836EF02B625040A4F"},"GPOE8P":{"Make Save Copyable NTSC Port - 1.0\/1.1":"7A4B3E596BA4FDA8F3FCF984079B3F44CB2D6AB3","Make Save Copyable NTSC Port - PLUS\/1.2":"304DB311F2107E88EBCE058535B266F9263AA8E2","Save Validation Code v 1.0\/v1.1":"9F6259132453DCC0607EAB35DB182C76818F2F9F","Save Validation Code v1.2\/Plus":"17EB6CFB408EF27D44C053A1336C3B87B6A05018"},"GPOE8Pr0":{"16:9 Widescreen":"99986BB1D4ABE45C429D48B01BFB578FEA69C14F"},"GPOE8Pr1":{"16:9 Widescreen (Plus) [Ralf]":"5563671A6702785FFE106DAB7EF3F4FE6D9F705B"},"GPOP8P":{"16:9 Widescreen":"DF133C61C29D5CFA87A2A785C962C205F80E6D04","Game Save Valid":"40EC1590DB26C7C58D00E3065F1E404341EF0E73","Game Save copyable":"2F5FB98927DAFF141D1CCD1EACA8AF626397E284"},"GPVE01":{"16:9 Widescreen":"E5B51080CFD518FE584CF3B61A1099CD032479D6","60 FPS":"6662E99E41F77763EEE524477DF341836EC7032E"},"GPVP01":{"16:9 Widescreen":"78F3C9A789827B063A6701987262276A66ABD82B"},"GQPE78":{"EFB Copy Fix":"880B114E9A308084CAB92C004A9EE067B371C310"},"GQPP78":{"EFB Copy Fix":"5D9A14954AE8D639C9B254F3BA73A70F284BBC8D"},"GQSDAF":{"16:9 Widescreen":"CA82C44B2E8FC5C184E3FF935BC89661B7DB55D6","60Hz":"952165FD78543EB6D2F5230F7570B0513773D332","No Blur":"DEB7DF358423F0EF30C9FD49F4F14590836D52A6"},"GQSEAF":{"16:9 Widescreen":"48AFE1D32843C6B5AD4337A49F15FD447DF2A752","Remove Blur":"EF448CF1FA6A15EB2661EA5338703C13D52EF65B"},"GQSFAF":{"16:9 Widescreen":"76C30E1737CC5C3B5DE32532B5F9A5CD50B690F8","No Blur":"986BAE5103CDE9286D2D34FD28FE2540D0759AAB"},"GR2E52":{"16:9 Widescreen":"5F55CE7DA7E1E3261CF2FF88F34CDA63BA511003"},"GR2J52":{"16:9 Widescreen":"D3C6FA690BED07BFA65C2E1FCDE2FFA5AA0EEEE4"},"GR2P52":{"16:9 Widescreen":"D7BCADD4E76B5E3F7D9D463F55A2777528E37E94"},"GRNE52":{"16:9 Widescreen":"483D74C47AD1012606D591A226AB3144C7FB201C"},"GRNJ52":{"16:9 Widescreen":"949281B7221B20680B7BC29E4754B73A326EBE85"},"GRNP52":{"16:9 Widescreen":"11051B094FE5A8B2E73060BFF786C1588E36979A"},"GROE5Z":{"16:9 Widescreen":"FADBAB5EC280CF8ED4C9536A33F4184BA210C9B0"},"GRYE41":{"Disable Culling to Fix Rise and Shrine Hang":"AF0A575EB6071EAC0D2EC3D2EA30A23EB05A4192"},"GSAE01r0":{"viWidth 704 Aspect Ratio Fix":"0D529180B9B28BB06E21EDBDBA61C17EEF0FFB7F"},"GSAE01r1":{"viWidth 704 Aspect Ratio Fix":"CB15ED22625690A5DBE4607FE30160125421461D"},"GSAP01":{"viWidth 704 Aspect Ratio Fix":"749EDA62B1B73354599929F1B50E992E111C5493"},"GSNE8P":{"16:9 Widescreen":"CFDF3FA5B48B347E49E8E521D7BE47FCC9A5CBF4","60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSNJ8P":{"60Hz Aspect Ratio Fix":"98DF9D67DE77F66A4B09F25C5621805DEB40865B"},"GSNP8P":{"60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSO":{"16:9 Widescreen Region Free":"0D60D0593F6DA28B1236381B22082506B9533F0E"},"GSPE69":{"16:9 Widescreen":"80E18A8963F4EFD3C03494C95934452FCB6E521B"},"GSPP69":{"16:9 Widescreen":"162E87E9F5511CD82216811055836C11B374C45B","60Hz":"E5DF2DF34D2F0BDEE4205C891576511EC572C5DC"},"GSWE64":{"16:9 Widescreen":"6480326CCA8FF64A4868EC6E9FDE9F38819A75A1","Disable Dithering":"85F65E4B627641862C1A6091D29A3BB0626B04C3"},"GTEE01":{"16:9 Widescreen":"6B57EBCB9CC0ACE2EDA0F13E3F1F55F3FABC3A26"},"GTEP01":{"16:9 Widescreen":"72785784363C46180AF8E388348FD77667E7D382"},"GTOJAF":{"Remove Blur":"53A2EDF113F2ED7E54A16AA0E73FDB2C44C79DE5"},"GTRE78r0":{"Limit internal FPS to VPS (improves performance)":"42B85EFC5DEF8B243521EAD434DDCBDF95664CB1"},"GTRE78r1":{"Limit internal FPS to VPS (improves performance)":"ADB3A41B992EAB15AE484B2E575ACF3354415186"},"GTRJ8N":{"Limit internal FPS to VPS (improves performance)":"47DBEFDCF22ADC2AB148A54AC885FE19B3C07E3F"},"GTRP78":{"Limit internal FPS to VPS (improves performance)":"E8752E1B60DF76CD0CC7408074E742B0966BC904"},"GTZE41":{"16:9 Widescreen":"ECCE87E0006475C73E7D936AA0150BD20166F9EF"},"GTZP41":{"16:9 Widescreen":"62D8F159B93167171E0860D96C4BB32A5BF5648B"},"GU2D78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU2F78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU3D78":{"EFB Copy Fix":"8A0E3114862ADFE421874211BD6F5220AA425BF5"},"GU3X78":{"EFB Copy Fix":"E3303FDAE7ECA17A72EDC440C32D94648A6453A0"},"GU4Y78":{"EFB Copy Fix":"D54767785E139A8BC8C4B75573FBD5A0B686D8E3"},"GUNE5Dr0":{"16:9 Widescreen":"C942EBCBE8A487C25E296EC1FAC2358DA1487DBD","60 FPS":"A441630EC5FF0EB74D2243A3092D22E69C6BEA02"},"GUNE5Dr1":{"16:9 Widescreen":"39BD84CBE2AFACFEDEC5E9020D1A8B0D36DD53F9","60 FPS":"05779F1A3D0C43305D52A95447D612CB424942C0"},"GUNP5D":{"16:9 Widescreen":"FE3BA1DAA1AF278A7839E27BF466B0BA0B390EC2","50 FPS":"3B20499A3F8D1D7CCA5B0015F2D80ECED25842A6"},"GV4E69":{"Fix 2D Rendering":"8679891FCAA250FCFF670B26E0CB9875900D17FD"},"GVJE08":{"16:9 Widescreen":"09EFDA75D876A675121C8344D8FDD09A70A1A846"},"GVJP08":{"16:9 Widescreen":"93938F4C6112C91549F2B52E6259170F7501EC07"},"GVPE69":{"Fix 2D Rendering":"3159CA79B0A890131763EA6CB163684BEE886E3F"},"GVSE8P":{"16:9 Widescreen":"4CE9C491160A4B631142EE9CE802C694163F1CA3"},"GWRE01":{"16:9 Widescreen":"AC42770B06662BE1DC863EC80F44B5E034C63664"},"GWRP01":{"16:9 Widescreen":"9DDDFAB28C4BD35CF64050E1EF684DC042B1AFFA"},"GWWE01":{"16:9 Widescreen":"98B2E75D8E1CED4A964D3129A5DC10E30538CAA6"},"GWWJ01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWWP01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWZE01":{"16:9 Widescreen":"9EFC191DE6D21A1681FE241AB2EE4A131259F317"},"GXCE01":{"16:9 Widescreen":"901A1E78A3A0124F55548507D3B3707125C64A8A"},"GXSE8P":{"16:9 Widescreen":"F50BBA440184FC77A4DFFAA58FF2BB888E2E072F","Aspect Ratio Fix":"4214C74DFB8A74FC3AA4A643E869BC4D9A38EDA0"},"GXSJ8P":{"Aspect Ratio Fix":"227909607984BBC3D36AAC7DB9DFE385F3363C49"},"GXXE01":{"16:9 Widescreen":"7CAFDAD7B5E7459CCC1BB209D439DDCBB5BE4E4E","60 FPS":"7E2829049003FC9DC8BC28044B33CAFF3768B54C","Allow Memory Card saving with Savestates":"64FAA15062F0D0C319F904BBDE9C4489A25D6369"},"GXXJ01":{"16:9 Widescreen":"AF704A8C6838FDF47054D9EDD9106B3F46781FA9","60 FPS":"4F5FD45B4EC1B33FE076FA06F8FF863C46A06BBA","Allow Memory Card saving with Savestates":"8293802260536FA2EF2EFDAB5266DE36BB88DE1B"},"GXXP01":{"16:9 Widescreen":"02716B2585BEE74F8FCDD97A78F6A0D3DC7F331B","60 FPS":"BF6171EA5CCF4B57AA596372589DDB5E6904DD7A","Allow Memory Card saving with Savestates":"3CAFBC4AE6FC5CE9F53377F86AB5BD8F1BC8861A"},"GYFPA4":{"60Hz":"402ED10AC842041AB4B39AE8F2D81B2D7AEF9CB4"},"GZ2E01":{"16:9 Widescreen v2":"E7521ED27BFC972628906CBE8D5403ED57253BB8","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2J01":{"16:9 Widescreen":"F985A0A58D8E2B23E8A557FAFF8D367AFFEADD07","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2P01":{"16:9 Widescreen v2":"CAC38B0D334B925A57AA3118D35932B8A185137E","Hyrule Field Speed Hack":"0F63623D4D984B7706F718F57C0ABDB6DBADCF8D"},"GZ3E70":{"16:9 Widescreen":"167F9E9A9B372CB3A01F308B46FF1403F8599C51","Deinterlacing Fix":"EF1291C0E45277CDAE4D0F61E1949C1E75E5A3E1"},"GZBJB2":{"Deinterlacing Fix":"19D67BD12A18F0E3A960BBB92E2F490BEF5C00C1"},"GZDP70":{"16:9 Widescreen":"2E424E0BBAE6EF5D6A8FB4224EA1D0746BAC37D3"},"GZLE01":{"16:9 Widescreen":"9FA864EE7DD8CE7FF538EB4E0243F20137430BD8","Remove Distance Blur":"78EA34CEF8E01701491C280F155F1C12EC9BF1A2"},"GZLJ01":{"16:9 Widescreen":"E03B61989025CA33937E63A057E6E40A403811BC"},"GZLP01":{"16:9 Widescreen":"3EFFF6C52B5633A1729FAA6883D8579E77F7D057","Remove Distance Blur":"14BECBEA4DD281EBD0F7FE7DEE8020B6F2418ACD"},"GZPE70":{"16:9 Widescreen":"7142F2495507AC7136992128ED0FCA6BC72B61F4"},"GZPP70":{"16:9 Widescreen":"591FD6C9668C79FF1CE8558EBED2486A7A327F05","60Hz":"A59B84DB5486521AEE1C23B6C741ECD35216E5AD"},"GZSE70":{"16:9 Widescreen":"8E86EAC7EA4F4D2854DD9020CD795630CA64C4EE"},"HAF":{"BufferPatch":"181195871F63B89B1CF09AFA4420CF89B9883108"},"HAL":{"RSAPatch":"AD12237401ABE9FE4A545AADB5C5AE10355E2076"},"P1RE01":{"60 FPS":"FD15A2EC38E46C1C3153336BC44F11B08ED906D0"},"R7XE69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"R7XJ13":{"Speed hack":"4590D425BCBAFA0D1E32D27B46068843F6A2BE09"},"R7XP69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"RB7E54":{"Limit internal frame rate in loading screens (fix hangs)":"BD1A252B43FD339391ED33554542E786BAD6671B"},"RB7P54":{"Limit internal frame rate in loading screens (fix hangs)":"90247F89DF8EB7A4AFA2C425CD3006257A95A8F8"},"RGQE70":{"crashfix":"5F4CF8D4DA19A0FF74FF9EB925AC0236069BFD59"},"RLEEFS":{"Fix crash on main menu":"793642AC6862C2F3412035A9E3D7172CC4A1D5C7"},"RM8E01":{"16:9 Widescreen":"E61344EB1542A78D497981C307B6549985C7A05A","Extra - Disable Music":"25223F9EFAABF601CAC7810004F124E4056598B1","QOL - Faster Boot Time":"BCC4279F8B28636AD773F01540E78DF40EAD6087","QOL - Increased Board Speed":"B65AF1819966CD3435D88801E8C79704E3A52DB5","QOL - Increased Taunt Capabilities":"66495D7CB532FAE778AFC22CF45D17D0FFDE5310","QOL - Increased Text Display":"B4186DDC54F33F4D6A22188EF50CEB43FB205673","QOL - Invert IR Stick for GameCube Mod":"9D90A9C66AE8AD91B201B40C4145D1323B701A77","QOL - Remove Explanations":"330DD53AB993A99576564FEFD222D7BD211B878F"},"RM8J01":{"16:9 Widescreen":"A140BDCB8E1721CB6B4CD878E412113322258B57"},"RM8P01":{"16:9 Widescreen":"F664C32AFD3D785FC6E04D8990A3FA1C72A18C5C"},"RMHE08":{"Bloom OFF":"CCF233DA57B3E75221870DE502955114B0D4E7FA"},"RMHJ08":{"Bloom OFF":"29D3625B7ED577587E56AA07CB0EB8C47C97E823"},"RMHP08":{"Bloom OFF":"1720C1173D4698167080DBFC4232F21757C4DA08"},"RO2P7N":{"Hangfix":"EEE9C8DE4671C18DD7F81DD08D39B64C57600DEA"},"RPBE01":{"Fix black screen effects":"775ABECA6073E02C5C68CF4D644194D966A418F5"},"RPBJ01r0":{"Fix black screen effects":"0EAB5D8DE827894AFEF97C10ACB67378E6983323"},"RPBJ01r1":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBJ01r2":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBP01":{"Fix black screen effects":"82AEB60F9A9083F93060531A970FFAABE0833A40"},"RRBE41r0":{"Idle loop speed hack":"20C7A2D4A3D6D55A8029A9D200D57CBD11F60B88"},"RRBE41r1":{"Idle loop speed hack":"0A30FA4C7E787197A00B7267368F372EFF623776"},"RRBE41r2":{"Idle loop speed hack":"2227787007F16F0CD454422AE6EB94656464B6D0"},"RRBJ41":{"Idle loop speed hack":"E9209EB728AA13375B2ABEF1C15D066DAFBF2EBF"},"RRBP41":{"Idle loop speed hack":"8A47BC311138EE3A11E049DF3EE6AED38990FB49"},"RTH":{"Disable blur":"812EE46AC967BFCD239335B10A664D71A93E8175"},"RUUE01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"B62652D5BBE0701900090CFEB32003AE6E5A5015","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"244D1FC7D6284DC8832CF7F34506B0DE4DBF8914","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"6FDD09D51EC3D754BA7F80630B1FE1D3FEF6FE97","QOL - Disable Grass Deterioration":"D566F2AF5BB1EDB8284461DEA85FF3B5A8DEBDD6","QOL - Enable Fast Text":"C7D8A7926ED952A2797DA312B41B08DCCDC3A274"},"RUUE01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"FC7BF1D6A5B4ED39EA4671C241E78F3BC8037E97","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"0235631B6F9A439B9718635F5AB7ADAFA2B6B3DE","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"5DEAC5F3390CE19487A9473A1988E7BB5EA4790F","QOL - Disable Grass Deterioration":"0FE1EF4DE5C8AE691E96634BC66BA034850C6C0F","QOL - Enable Fast Text":"B2BFF779434FDBEF08FFD73C4B140FCE0A8FDF0E"},"RUUJ01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"D79F773E1363B694784C94DD309709D41D368A08","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"C88CFD7E6C18F570DE974D6C0202987E4F1B72C4","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"3C92010E7DE2E93C48AFF25B30A18BBFCBD12707","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUK01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"1B89E8A6E6E09757373FF116BBC0989C06651709","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"EF883FB0C237BF51ABD88C35D34571D240C79484","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"F08D0F233F3F5833DFAFE5C8049C37D32360F0FA","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"C55A5CDEF4C868E66C1FB2902D3285B2C350D7F1","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"D28D4E4FBD153A6D4C49E2D250213F37BEC941E5","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"80C3F50C018E076DF2CD39732A5ABB8A1C0E88B1","QOL - Disable Grass Deterioration":"3B278C19AC76D27488C55201BA0DB63864F66AAF","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"00D994940AD323786A9DA780D1347AE5FBABAC8A","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"9121BEDD72436F4EAB47E4CE96ABD864118B22F3","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"8FC58B3DC4817515822EBB82BB7A8055270EE51D","QOL - Disable Grass Deterioration":"9B199D77AF748518358D32966F3F55F5C5759C3A","QOL - Enable Fast Text":"827CF5DFC90A258C36BF32DC3883371973E0B706"},"RX4E4Z":{"Fix file reads (dcache bypass)":"9E4E0F1465A9A1E85349DBA3B1278AC215A97DBB"},"RX4PMT":{"Fix file reads (dcache bypass)":"EE85907C03F0295794821383B93F8D5B91D2697A"},"RZDE01r0":{"Hyrule Field Speed Hack":"15EAD073414C9903D6CAE5229DCE582BD17A9162"},"RZDE01r2":{"Hyrule Field Speed Hack":"27395CC8BC2C51201D566657D31A471A850482FB"},"RZDJ01":{"Hyrule Field Speed Hack":"B3F7473F8C911A32F1D616491C9E78EBBD7A6309"},"RZDK01":{"Hyrule Field Speed Hack":"A280C0114B800D7DC056ECFB5E482229DA0B1550"},"RZDP01":{"Hyrule Field Speed Hack":"2A83ADFB760F9498841ED0ED68B0C0438232472C"},"SAOE78":{"Fix crash on boot":"EA11FA4908FB20B61876ACD360EC7657A6D39FB2"},"SAOEVZ":{"Fix crash on boot":"AA55C214DE7545DE0E203CC39F06BF3D31451BE9"},"SC2":{"Limit internal frame rate (speed hack)":"B3FB0FCE108E85E6198BAF3535850470049E2BE2"},"SGLEA4":{"Fix black screen":"258378187ACF475A55EFEAF8A703681252E014C3"},"SGLPA4":{"Fix black screen":"6F8CD59D897338CA90939149E1A62588620C6D88"},"SX4E01":{"60 FPS":"206E79C5E9B5E07CEB7F52D0F5E5FF3400009265"}} \ No newline at end of file +{"D43J01":{"loophack":"CAB9CED2D904F12CCB21F5B1DE9B5433620C3E13"},"DPOJ8P":{"Bypass Modem Detection":"90BA66E25640A538FEFA6693715718139079FA7B"},"G2BE5G":{"Disable interlaced rendering":"7FFF6BDD93713BEDFD23739C32B86153FA19AEA0"},"G2BP7D":{"Disable interlaced rendering":"56E85D7285F10348E1E5354E379918D07E79EDA9"},"G2VE08":{"16:9 Widescreen":"3ED033396382424533A263A39687A0961EC994A6"},"G2VP08":{"16:9 Widescreen":"EEBC3C5F04BB70AFCA1A4C3D638335CDC9142BA2"},"G3RP52":{"16:9 Widescreen":"7F6B7743F0DDD3704B15882330E490103AE24E36","60fps":"4C9A8572A35F1DB85BBB2F32ED34288A343F1FC0"},"G3YP52":{"16:9 Widescreen":"3D102AFEE2F4945894E74BB702ED3ED2294C6FEC","Force PAL60":"FD61581C593EA34DD358AE33819FAB310F2CE961"},"G4AEE9":{"16:9 Aspect Ratio Fix":"8C2564CF47BB6A9B57D712C90E17F459B0BB6CCD"},"G4ME69":{"16:9 Widescreen":"6FC6E5D91693FAABE25CC721F2121CB0B5CA9D12"},"G5SE7D":{"16:9 Widescreen":"16119398BFE241A84151C046E0BC5BCF89D22049","Remove Bloom":"A5F2FB2B3BFF61F34F8AB1DD5A912A0FD8DFA720"},"G5SP7D":{"16:9 Widescreen":"718AA8BD831961C33114852FDA75BB58F768F46A"},"G8ME01":{"16:9 Aspect Ratio Fix - Centered HUD":"8C2FC3FC6F7766AA8E87425BF3BDD2E46E73DE71","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"AE3DBE2604CA0660165CA870C4B80E2B523C9475","16:9 Aspect Ratio Fix - Normal HUD":"F6E8264CF14D0189219C1E2D20C4182F190F4B88","16:9 Aspect Ratio Fix - Stretched HUD":"8EA4C7B80ADE3E8FEC28EEFA3A046B6B04162E81","No Letterbox":"B1DE2A3526F2E4511F2B94A5171BF7B3C2C52B9B"},"G8MJ01":{"16:9 Aspect Ratio Fix - Centered HUD":"B3181CA1AAB11EAE9132184DA48D76E775AC0A24","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"904C0EEEF16D761F7BDF7099DEB384DD9AB26E00","16:9 Aspect Ratio Fix - Normal HUD":"631CE99BDE3F1475303C00C67E3C130D4EB2547B","16:9 Aspect Ratio Fix - Stretched HUD":"81F7AD591F12A01DE82850457ECF147998D1238F"},"G8MP01":{"16:9 Aspect Ratio Fix - Centered HUD":"75AF5D7FA49BE9C3C73EA1482B49CD9B36CE817E","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"C2E5003E74551DF693A7CB814850CA51D77549EF","16:9 Aspect Ratio Fix - Normal HUD":"FB26AA84B0CB787705E84E869B649307776A2839","16:9 Aspect Ratio Fix - Stretched HUD":"C5F8A81520193418C07C7BC4881F02BE9C4DC9BC","No Letterbox":"7828F5B0C8B470620755C16C60A5A4554F92640C"},"G9SE8P":{"16:9 Widescreen":"CD043229AC47AC93D537B6A0725A2D0BB6FEF4E8"},"G9SJ8P":{"16:9 Widescreen":"C55EF90B542AA3973352A232629A828BC22A8509"},"G9SP8P":{"16:9 Widescreen":"9521774ADEE769CFCCE86F8170FE626387E73019"},"GAFE01":{"16:9 Widescreen":"D24F6D9BE5D79D3C795C2844634A0214BB68B5DA","Make Game Save Copyable (donny2112)":"F9A812FF62A20D5440CAB79DB6AFAE068457A10E"},"GAHEGG":{"Limit internal frame rate (speed hack)":"2873E5178C8068AFA8AFE751F903A69CA9A41F16"},"GAL":{"Trophy Viewer":"DBDB50216B22CDFFF77BDD80261CD0FF75803D3B"},"GALE01r0":{"Normal C Stick Functionality in Singleplayer Modes":"9DC8D90D986FAC175F45006AFF20CF00EDFBE053","Proper 16:9 Widescreen Support":"18B4159E77013EDFE499A33281B771C442970E3B","Properly Display in 4:3":"DC2BCA7D99EBDE1DC7C68CA6AC7F543B93FD6A67"},"GALE01r1":{"Normal C Stick Functionality in Singleplayer Modes":"F6030959C1CAC92F37287D556F18B3C5298FAD5B"},"GALE01r2":{"Disable Rumble":"CFDEEA8AD2A99394BFFEA7211EF03394E7471FA9","Normal C Stick Functionality in Singleplayer Modes":"BCD918A72368B2A30E797C76994E7539A1C0B5D9","PAL Stock Icons":"6B92D95F0EEA165EA84FD753F74B0A9E2079C2D7","Proper 16:9 Widescreen Support":"CEDF18354CDCFC49398551C629E51B053331A55B","Widescreen Support v1.2 [Dan Salvato]":"D32ED4CD685F3FF649F7E749E8C182C657013918"},"GALP01":{"Normal C Stick Functionality in Singleplayer Modes":"4817F202678559291F9F76B46DC7561A45D00B77","Proper 16:9 Widescreen Support":"BFCBE098FE8679B9140A48EA1D8620D5A369697C","Properly Display in 4:3":"5BA2933B8365E21553AA273B91954A4C722B1035"},"GAZE69":{"60FPS":"BC281053C5266B21FDDC6C8996A6021DEA03D459"},"GBLE52":{"16:9 Widescreen":"C0F3AB3BBC9B21F4BF25F44E48CF1CD36D2B3F45"},"GBLP52":{"16:9 Widescreen":"797040CB097BFD369490A1BE29659929D76CE6F7"},"GBRJ18":{"16:9 Widescreen":"B3C867AB34D90E2E9C5B799D800F4C191DE279FA"},"GBSE8P":{"16:9 Widescreen":"4148D453F61A5B81E53669335BC667E651865C76"},"GBSP8P":{"16:9 Widescreen":"6CB029AE768BA5B9995D5F12C1EBB37B3D1FE242"},"GC6E01":{"16:9 Widescreen":"DE932CCAD1B5EB1FAA8D08733BD0D3940C78382A","60 FPS [Nerdzilla]":"85F98064F17AFE0FB02A99A182AF3FEB7644DCAA","Allow Memory Card saving with Savestates":"2F64F98686E62B60E466E931A9EBCD19A750FF4E"},"GC6J01":{"60 FPS [Nerdzilla]":"A6146772E37970EA5564F51A622A04792158B321","Allow Memory Card saving with Savestates":"D8F327304A88FBC717BB1F775494C5F864B9E8D2"},"GC6P01":{"16:9 Widescreen [Ralf]":"05C9016BF58A628A3FD7B16E1B9BB0769A14846F","60 FPS [Nerdzilla]":"1D3FB017C0D20754E48A8BA54EEE7DC4851BE84B","Allow Memory Card saving with Savestates":"EDEE0E28EEA1834868F2865336290FFBDFB9C6DA"},"GC8JA4":{"Fix C4 texture tiling (used for buttons and some character icons)":"843297019804192AF1FC660BC85F4B10891D0BA3"},"GCBE7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD"},"GCBP7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD","60Hz":"DF2C4DDF082C9E67919443C5ACC485AB2CF7F8D0"},"GCCE01":{"16:9 Widescreen":"BA0066518E7EA8599A81F8496BD39DE20CC369D6","Fix GBA connections":"483BDB94615C690045C3759795AF13CE76552286","Fix buffer overrun bug (crash at Goblin Wall)":"6C107FEC15C76201233CA2645EB5FAB4FF9751CE"},"GCCJGC":{"Fix GBA connections":"4C104D24329172F5D0F8649DE9423B931FE72CA3"},"GCCP01":{"16:9 Widescreen":"2580E7379633CF4DE13B9EC945DB17543477828A","Fix GBA connections":"2EAA60A8A115AD68A795109FB59E4A726D29016D"},"GCN":{"Fix C4 texture tiling (used for buttons and some character icons)":"601FE183C9524ACCF068874DABD73921C86769CF"},"GCVEEB":{"16:9 Widescreen":"ADA686C90189D1D7D23E23E525EFC96EFA104BB9"},"GD7E70":{"Deinterlacing Fix":"6F8B6E33214162E5CDEBD2BC6A60FAFA04D2EB10"},"GD7PB2":{"Deinterlacing Fix":"3B798C9D63E755349B253FAF9186BAE1F703D96A"},"GDREAF":{"Fix audio issues":"F8EAE60FEB0CFB4477FDC4B9E136B63F68DFA63A"},"GDRP69":{"Fix audio issues":"E23D98B2CE185C3993A40F2495D37E41B971BF91"},"GDSE78":{"Limit internal frame rate (speed hack)":"BE8F752F2A2660380B2AC5953F394DD867880EB3"},"GDSP78":{"Limit internal frame rate (speed hack)":"5F8EF8AA8C08D7B3173A59534E8482804C0446D9"},"GE4":{"Limit internal frame rate (speed hack)":"9ABA0E4C533142BBA35DD87180EE38CAE3711226"},"GEDE01":{"Fix startup hang":"21068C3CE905FB0CFFAA7408A93154AF8A5295A2"},"GEDJ01":{"Fix startup hang":"7061F3CF11BF64D3BA7F32CCF2BAC42FF3614AB6"},"GEDP01":{"Fix startup hang":"6F1B00517CBA30BEB738EAA90E71221378CD570D"},"GEME7F":{"Force Progressive Scan":"CB04E00918C9C0F161715D21D046ED6620F7ADEF"},"GEMJ28":{"Force Progressive Scan":"41D8F2C833C0B7B79FCA038752052059207AE4D5"},"GEZE8P":{"16:9 Widescreen [gamemasterplc]":"5CEBCFBEA4E444495D2FD6D8B6607DFB2349CC1B"},"GF7E01":{"16:9 Widescreen [gamemasterplc]":"C59CCA3EF8A5E97B32EB64DB9AE80E652ED281C6"},"GF7P01":{"16:9 Widescreen [gamemasterplc]":"1230053B5C347B62E432EFD6635433A183D18619"},"GFQEA4":{"16:9 Widescreen":"5CD9568CE58EF82EB371594667015C9B799454B9"},"GFZJ01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GFZP01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GGTE01":{"16:9 Widescreen [darkludx]":"EF128E7A9C22676834F558BA0F0F7FD8B9028727","Experimental 60fps":"D40344111D989EA009901F8B1C45B5AC8D39E6D2","Simple 60FPS":"5232A937D1D813FF58DD71D716284ED6AB535689"},"GGTJ01":{"Experimental 60fps":"BEC2310911003AF9E4B984A4051E0F885B8CC825","Simple 60FPS":"D62E777A2F019D7BD0AFFBCE876BD9AE408F1667"},"GGTP01":{"16:9 Widescreen":"FA9CD330ECDA01275DA88BD0803DE47757D16A4E","Experimental 60fps":"EB1ACD613BB432A3391CFFF1F0145A6ACE66B210","Simple 60FPS":"43FAD0DDD130BE1E4C8C7603EF6CA7DAA0DF5EE9"},"GGVD78":{"16:9 Widescreen":"9D07DBB5EC2FAA47F2E4587FBD75FD6F3E7E91B0","EFB Copy Fix":"FE52240DF6D132C15A8324E8A477F2BF2250D208"},"GGVE78":{"16:9 Widescreen":"86E561452235BF88D41884558EF34F54CE0FEB48","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVP78":{"16:9 Widescreen":"F9B2108D833084FA36A53E00F1647A9579F847D3","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVX78":{"EFB Copy Fix":"740F2D1C01DA39D1760D96B03974A48E6F74578D"},"GH2E69":{"Limit internal frame rate (speed hack)":"C874194B083036964F8B2606BA680C646F642D96"},"GH2P69":{"Limit internal frame rate (speed hack)":"D437A732A1907B3F32FA6C2C64F40A80032878B5"},"GHAE08":{"Fix audio issues":"9799AFF8463EC86C9230E31E2627E141F0C129D3"},"GHAJ08":{"Fix audio issues":"B45A8FC32D14567B8D6C95F303E00A72C0E1D344"},"GHAP08":{"Fix audio issues":"BC7F3CFC97593AA2055C370C175950DC478D2709"},"GHKD7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKE7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"9BFBDE9184BC05EBD545625DEE486E3AAB36B6A3"},"GHKF7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHKP7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKS7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHLE69":{"16:9 Widescreen":"742BFC79D8F0BA5D6215772B58F5A0EADD7BFAFF"},"GHQE7D":{"16:9 Widescreen":"520E5F718D3BD1291C55021776091E0DE0FB4822","Limit internal frame rate during startup (speed hack)":"F6BF450D22104FD2527122EA6E81B1F70E916B95"},"GHQP7D":{"16:9 Widescreen":"AAE14CDC13A0C082A850DED85417100A8C8A0687","60Hz":"884A33613AE8D916128E3FF39B3AA9F63DAADC58","Limit internal frame rate during startup (speed hack)":"BD475ADEF145AC9226514CD1B2D4D5F3BFE67964"},"GHSE69":{"16:9 Widescreen":"3538CDBD13FA939B7BA5F0F82253401AFDD1E2CD"},"GHSP69":{"16:9 Widescreen":"409754E2EBB6F05DEE1AFC647E25B15D462638FF"},"GICD78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICE78":{"EFB Copy Fix":"5BF55685B8867A85EAA9C86571309B17BF7DED32"},"GICF78":{"EFB Copy Fix":"85AABAEB9A59C4F96D9330A3B884F6D757DA1683"},"GICH78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICJG9":{"EFB Copy Fix":"969134EA21A160EBDA91C0870266E7D1707FDC43"},"GICP78":{"EFB Copy Fix":"13B158CF41F5412BC637F50644193D43CC3DA49A"},"GIQE78":{"EFB Copy Fix":"E15AA1E30D26E5735D68AAADE436E7B7E4A33A35"},"GIQJ8P":{"EFB Copy Fix":"FFFCB76E98DDB06A7BBBC0AA73C869C87EB787D6"},"GIQX78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GIQY78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GK2":{"16:9 Widescreen [Vague Rant]":"6B4C59C2A2C5D71C7A38C8513A76E6467E307A26"},"GK2E52":{"60 FPS":"8F2397930A50C3C176188526EB9612578F9CBCE5"},"GK4":{"16:9 Widescreen":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKB":{"16:9 Widescreen Region-Free":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKBPAF":{"60Hz":"8E4E0ABA0E6D102A33206F34F4DEE63159B3CB4F"},"GKDP01":{"16:9 Widescreen":"DF4657937DE730107B636C73E959277EB963D210","60Hz":"6740D7B914CC897AF50798C0AB391965294FD1E1"},"GKRPB2":{"16:9 Widescreen":"69C01A4F91D991CF866237FED94A138308FC104F","60Hz":"40D06A5C7A3D873ABC73DEDF573738C2E3E37FF0"},"GKWJ18":{"16:9 Widescreen":"696570101FA040778EAD310377C484C846D87430"},"GKYE01":{"16:9 Widescreen":"6520EF1B7D88F00747B120A3A010458602ED989D"},"GKYJ01":{"16:9 Widescreen":"47B8EF7D02831AA5C375C698797BF2D5475FEFF2"},"GKYP01":{"16:9 Widescreen":"69413C75036D2975716066E6574461B981FF0124"},"GLEE08":{"Fix audio issues":"7355F358CAC6F418D37E4C23E64F7867D46E4FC9"},"GLEJ08":{"Fix audio issues":"12B24A6D7389A2AC5AB75FC0BF8493E7661F2A73"},"GLEP08":{"Fix audio issues":"81BD39F5527552DE89E3B59BA86298900F0A3168"},"GLSD64":{"Fix freeze in opening cutscene":"5E2A73717BD66EF647846DD64C33BC80AD9B5227"},"GLSE64":{"Fix freeze in opening cutscene":"1CE78E7954415A44DF693C0BB879AA5A4FF059A3"},"GLSF64":{"Fix freeze in opening cutscene":"009B0C4AD80A9C28C987934D254C2C4AACC9A07A"},"GLSP64":{"16:9 Widescreen":"8E7A544C10E7A5E2F0304A0D2586879627EF6586","60Hz":"B67144E87B54246137142992A3BC83DC13BE68A7","Fix freeze in opening cutscene":"3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC"},"GM4E01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"78BE50F93E81A1972CA31ABC318949E366E82BD0"},"GM4P01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"6D950B6EEFD6D304E1A424355B74A595D32BDF2F"},"GMBE8P":{"16:9 Widescreen":"D12DE9E3941BCE6EB50B8DA92140E09AFC0104C8","NA Stretched HUD":"0151A6683E1FBFD20096807C54A6952C4A4BAD40"},"GMBP8P":{"16:9 Widescreen":"8A688981F33A2C53882EF08FCF20B88AD43EF417"},"GMNE78":{"Limit internal frame rate (speed hack)":"DE38BC8493EA24F93B6D805F28746AA99C53C677"},"GMNP78":{"Limit internal frame rate (speed hack)":"65FB9B430F7D58EBC76DAADE8CA9DBBAA32B8B51"},"GMOP70":{"16:9 Widescreen":"CE4D298616BD42E4D8F8936B325CD1FD7F5B956B","60Hz":"0CD2CC787A5FF551901E41C85C8AAC02017ECB89"},"GMPE01":{"16:9 Widescreen":"4F98592DB3DEE3857469A8D8605FAF4BD6F7C76D","QOL - Allow Digital Presses for Map Screen":"E232B27564E9AA0C32DE163C9C056317A7B2B12E","QOL - Automatically Advance Text Boxes":"59607671BFC4717ACAF9807BB7EB0D9F982866D4","QOL - Disable Advance on Results":"CAEA37F3FEF89400513353EB85875F2A3AF4C03E","QOL - Faster Boot Time":"F75FBBD838C5B84FF687962FBA9195B217DE132C","QOL - Increased Board Speed":"19F264DE7F07EAC9433CA4B591BEBA1EC976C9F1","QOL - Increased Taunt Capabilities":"C70391D4961A0E820DE40141C89341369A9F021B","QOL - Instant Text Display":"F4E6913CF034E5778B9F9CA5FF448FF1B78B3333","QOL - Rumble Always Off":"93621947019532F02C25937FD3EEEE21A788CB07","QOL - Show Controller Port Number of Who Paused":"D7249AC3C949FEB860CF4350C5B7B79AC16569F1"},"GMPJ01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMPP01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMSE01":{"60FPS":"2805B1A0DD53EB64877D375D10F667700955720F","Widescreen":"BD718F961DBA5372B1D0257D454D535746C453A0"},"GMSJ01":{"16:9 Widescreen":"E8112A01040A06460E368F142C5D1FD0B4085D94"},"GMSJ01r0":{"60FPS":"9894B5B6B5215844D48411021FF8EECFE851D79D"},"GMSJ01r1":{"60FPS":"2DB69DB7A3753D543CD85CA4C77B4EF0AED7486E"},"GMSP01":{"60FPS":"D07009B710B3BBC6B12E54ED6A7969D58197EDAF","Widescreen":"0B7B89BF0868393076EF5F9DBD689DD0EDBCB84C"},"GNHE5d":{"Nop Hack":"89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF"},"GOCE5D":{"16:9 Widescreen":"E4D800B90F16A15D9252EED1B2A23AEAF5CD230A"},"GOME01":{"16:9 Widescreen":"C33CBBF6F22195BF626104B4827200B3EE1CF30C"},"GOMP01":{"16:9 Widescreen":"874784CCD4AC3DC9C69654E471C18AD0F5E4D9AE"},"GONE69":{"Widescreen Hack Culling":"6412A67E1C26E146A9526AFC9F0EE517B214069D"},"GP4J18":{"16:9 Widescreen":"BF4D2D6AF66F285E81173B3B20D5369DB6FB351B"},"GP5E01":{"16:9 Widescreen":"5ED053787332DB5EEB8B963BA5DF197B58C6BB75","QOL - Automatically Advance Text Boxes":"41EF84663008668682BB75DC8B874E38669AE041","QOL - Disable Advance on Results":"6C36BEFE751131298BC8B39012892F6A6903CE60","QOL - Faster Boot Time":"8E952E6E12639AC20F6F9FDD150C12F0C294DF43","QOL - Increased Board Speed":"4BD72B8ED75ED8EFB3329C104BAD114FDDB8AD2C","QOL - Increased Capsule Throwing Speed":"C5BA3C9D386D09DD6CB9242A12A0D041BEB93160","QOL - Increased Taunt Capabilities":"C360EBDDCB0237076052C983BD9719411CA67CDE","QOL - Instant Text Display":"2CD4A06A3B9C18CC9EA2A2DEBD96E066CD1D700A","QOL - Show Controller Port Number of Who Paused":"5BA3648D18346EDC97B23475D4748C637E6095A2"},"GP5J01":{"16:9 Widescreen":"81E69B6BEE85E06805EC24E1CFEA9405BF459DF8"},"GP5P01":{"16:9 Widescreen":"FD4FCCB97C06F0173A30D4CC9A99422B2C0ABA9B"},"GP6E01":{"16:9 Widescreen":"B502AF0887792E9B140516D14BA75F99772C17BE","QOL - Auto Advance Text Boxes":"E645DF70CE0266D174D917A817A7FBEEE974A9F0","QOL - Disable Advance on Results":"798E566B621F6A32BF23C588E76EAC0776E2A8A3","QOL - Faster Boot Time":"B089A3C80D5DA86ABF6258F8BFB1DC78BB54A973","QOL - Increased Board Speed":"501C52ACF117950888A66DB2BEDBDFD7D9B20235","QOL - Increased Orb Throwing Speed":"845F439278DA8CBC225658FF4E2F82D707422BFB","QOL - Increased Taunt Capabilities":"0AED27341A06186AD92D5725C0B9693CA8F85B7B","QOL - Instant Text Display":"5AAC4E21927D4AEE7AC329CE793D4CDAEDDF7A3F","QOL - Show Controller Port Number of Who Paused":"DF5D2F068D76ABF2B1C294D413B9154B8C356929"},"GP6J01":{"16:9 Widescreen":"88F50F8298F82E3E5C161277BD4A985CE893A9D0"},"GP6P01":{"16:9 Widescreen":"DCCE59CA987624C753F0A86BEBA4287635901C93","Remove Black Bars":"138A86D43B5830BFE1926F58C0A4FBE2971BB02D"},"GP7E01":{"16:9 Widescreen":"87EBF72D90503EBC5CE9BC994795669E48A92A4A","QOL - Auto Advance Text Boxes":"82338AB5D82A7F398B4EB674CA4866DD1F4A1E65","QOL - Controller Options Always Acesssible":"D2F9CC0B9E52136E77187395FB7DA9B64B2C3B30","QOL - Disable Advance on Results":"A644551162A61DE7997B2A84F5BFDEFFD3FC2FB5","QOL - Faster Boot Time":"1939B5C575AE8FF6890BDDDD1B3FEA472D8CABA0","QOL - Increased Board Speed":"7E06A2EBCFECADB8074152B489984237A6FE9F21","QOL - Increased Orb Throwing Speed":"6671BEE71920D6B845AE1257AFE3AB3B95CD6FBD","QOL - Increased Taunt Capabilities":"9F60923F95FFE709CCD06966CD2DF743263934D1","QOL - Instant Text Display":"48FA6002E42DA9BB83ABC980A32C60CE3B21C4FC","QOL - Show Controller Port Number of Who Paused":"5E6F8C6C87D5944DB18A4E4F9A45F61EE87A4163"},"GP7J01":{"16:9 Widescreen":"A081F0729068D233E99DC00C64E36CA33C945640"},"GP7P01":{"16:9 Widescreen":"75D6CDA4EA301A71F7701A0487544E79136850B9","60Hz":"6D0D5B7CA58C38A3147F8ED981B5B21CC6C519CD"},"GPIE01r0":{"16:9 Widescreen":"2B58166A66644F984E64077A6245C94C79B51063"},"GPIE01r1":{"16:9 Widescreen":"9A1545AB4E2B9216B95AFFA83420D723512DA8C8","60FPS":"3337C767EAA21D30C6CFEAE9985FFCABFC63E2E3","Disable Pikmin optimizations":"CBD15E9104929F0631713AE482475CB2B90E71F2","No Blur":"A25CDDB79991F090BD7F12840AD06F13BD5CCF95"},"GPIP01":{"16:9 Widescreen":"5AE44D5B0E0D4C412B453B95CF5A41DBA4D685E6","60Hz":"AE59FF751E167632AE283F38CC4B96BDF7B9B81F","Turn off blur":"4ED79A548DBD7DBD35928A2F1138315FF103E260"},"GPNE08":{"16:9 Widescreen [Ralf, darkludx]":"ED3D9BB08C5F1D21BC8D5C73155C00E3C25C466F"},"GPNP08":{"16:9 Widescreen [Ralf]":"469B18FE8219031D355557B836EF02B625040A4F"},"GPOE8P":{"Make Save Copyable NTSC Port - 1.0\/1.1":"7A4B3E596BA4FDA8F3FCF984079B3F44CB2D6AB3","Make Save Copyable NTSC Port - PLUS\/1.2":"304DB311F2107E88EBCE058535B266F9263AA8E2","Save Validation Code v 1.0\/v1.1":"9F6259132453DCC0607EAB35DB182C76818F2F9F","Save Validation Code v1.2\/Plus":"17EB6CFB408EF27D44C053A1336C3B87B6A05018"},"GPOE8Pr0":{"16:9 Widescreen":"99986BB1D4ABE45C429D48B01BFB578FEA69C14F"},"GPOE8Pr1":{"16:9 Widescreen (Plus) [Ralf]":"5563671A6702785FFE106DAB7EF3F4FE6D9F705B"},"GPOP8P":{"16:9 Widescreen":"DF133C61C29D5CFA87A2A785C962C205F80E6D04","Game Save Valid":"40EC1590DB26C7C58D00E3065F1E404341EF0E73","Game Save copyable":"2F5FB98927DAFF141D1CCD1EACA8AF626397E284"},"GPVE01":{"16:9 Widescreen":"E5B51080CFD518FE584CF3B61A1099CD032479D6","60 FPS":"6662E99E41F77763EEE524477DF341836EC7032E"},"GPVP01":{"16:9 Widescreen":"78F3C9A789827B063A6701987262276A66ABD82B"},"GQPE78":{"EFB Copy Fix":"880B114E9A308084CAB92C004A9EE067B371C310"},"GQPP78":{"EFB Copy Fix":"5D9A14954AE8D639C9B254F3BA73A70F284BBC8D"},"GQSDAF":{"16:9 Widescreen":"CA82C44B2E8FC5C184E3FF935BC89661B7DB55D6","60Hz":"952165FD78543EB6D2F5230F7570B0513773D332","No Blur":"DEB7DF358423F0EF30C9FD49F4F14590836D52A6"},"GQSEAF":{"16:9 Widescreen":"48AFE1D32843C6B5AD4337A49F15FD447DF2A752","Remove Blur":"EF448CF1FA6A15EB2661EA5338703C13D52EF65B"},"GQSFAF":{"16:9 Widescreen":"76C30E1737CC5C3B5DE32532B5F9A5CD50B690F8","No Blur":"986BAE5103CDE9286D2D34FD28FE2540D0759AAB"},"GR2E52":{"16:9 Widescreen":"5F55CE7DA7E1E3261CF2FF88F34CDA63BA511003"},"GR2J52":{"16:9 Widescreen":"D3C6FA690BED07BFA65C2E1FCDE2FFA5AA0EEEE4"},"GR2P52":{"16:9 Widescreen":"D7BCADD4E76B5E3F7D9D463F55A2777528E37E94"},"GRNE52":{"16:9 Widescreen":"483D74C47AD1012606D591A226AB3144C7FB201C"},"GRNJ52":{"16:9 Widescreen":"949281B7221B20680B7BC29E4754B73A326EBE85"},"GRNP52":{"16:9 Widescreen":"11051B094FE5A8B2E73060BFF786C1588E36979A"},"GROE5Z":{"16:9 Widescreen":"FADBAB5EC280CF8ED4C9536A33F4184BA210C9B0"},"GRYE41":{"Disable Culling to Fix Rise and Shrine Hang":"AF0A575EB6071EAC0D2EC3D2EA30A23EB05A4192"},"GSAE01r0":{"viWidth 704 Aspect Ratio Fix":"0D529180B9B28BB06E21EDBDBA61C17EEF0FFB7F"},"GSAE01r1":{"viWidth 704 Aspect Ratio Fix":"CB15ED22625690A5DBE4607FE30160125421461D"},"GSAP01":{"viWidth 704 Aspect Ratio Fix":"749EDA62B1B73354599929F1B50E992E111C5493"},"GSNE8P":{"16:9 Widescreen":"CFDF3FA5B48B347E49E8E521D7BE47FCC9A5CBF4","60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSNJ8P":{"60Hz Aspect Ratio Fix":"98DF9D67DE77F66A4B09F25C5621805DEB40865B"},"GSNP8P":{"60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSO":{"16:9 Widescreen Region Free":"0D60D0593F6DA28B1236381B22082506B9533F0E"},"GSPE69":{"16:9 Widescreen":"80E18A8963F4EFD3C03494C95934452FCB6E521B"},"GSPP69":{"16:9 Widescreen":"162E87E9F5511CD82216811055836C11B374C45B","60Hz":"E5DF2DF34D2F0BDEE4205C891576511EC572C5DC"},"GSWE64":{"16:9 Widescreen":"6480326CCA8FF64A4868EC6E9FDE9F38819A75A1","Disable Dithering":"85F65E4B627641862C1A6091D29A3BB0626B04C3"},"GTEE01":{"16:9 Widescreen":"6B57EBCB9CC0ACE2EDA0F13E3F1F55F3FABC3A26"},"GTEP01":{"16:9 Widescreen":"72785784363C46180AF8E388348FD77667E7D382"},"GTOJAF":{"Remove Blur":"53A2EDF113F2ED7E54A16AA0E73FDB2C44C79DE5"},"GTRE78r0":{"Limit internal frame rate (speed hack)":"42B85EFC5DEF8B243521EAD434DDCBDF95664CB1"},"GTRE78r1":{"Limit internal frame rate (speed hack)":"ADB3A41B992EAB15AE484B2E575ACF3354415186"},"GTRJ8N":{"Limit internal frame rate (speed hack)":"47DBEFDCF22ADC2AB148A54AC885FE19B3C07E3F"},"GTRP78":{"Limit internal frame rate (speed hack)":"E8752E1B60DF76CD0CC7408074E742B0966BC904"},"GTZE41":{"16:9 Widescreen":"ECCE87E0006475C73E7D936AA0150BD20166F9EF"},"GTZP41":{"16:9 Widescreen":"62D8F159B93167171E0860D96C4BB32A5BF5648B"},"GU2D78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU2F78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU3D78":{"EFB Copy Fix":"8A0E3114862ADFE421874211BD6F5220AA425BF5"},"GU3X78":{"EFB Copy Fix":"E3303FDAE7ECA17A72EDC440C32D94648A6453A0"},"GU4Y78":{"EFB Copy Fix":"D54767785E139A8BC8C4B75573FBD5A0B686D8E3"},"GUNE5Dr0":{"16:9 Widescreen":"C942EBCBE8A487C25E296EC1FAC2358DA1487DBD","60 FPS":"A441630EC5FF0EB74D2243A3092D22E69C6BEA02"},"GUNE5Dr1":{"16:9 Widescreen":"39BD84CBE2AFACFEDEC5E9020D1A8B0D36DD53F9","60 FPS":"05779F1A3D0C43305D52A95447D612CB424942C0"},"GUNP5D":{"16:9 Widescreen":"FE3BA1DAA1AF278A7839E27BF466B0BA0B390EC2","50 FPS":"3B20499A3F8D1D7CCA5B0015F2D80ECED25842A6"},"GV4E69":{"Fix 2D Rendering":"8679891FCAA250FCFF670B26E0CB9875900D17FD"},"GVJE08":{"16:9 Widescreen":"09EFDA75D876A675121C8344D8FDD09A70A1A846"},"GVJP08":{"16:9 Widescreen":"93938F4C6112C91549F2B52E6259170F7501EC07"},"GVPE69":{"Fix 2D Rendering":"3159CA79B0A890131763EA6CB163684BEE886E3F"},"GVSE8P":{"16:9 Widescreen":"4CE9C491160A4B631142EE9CE802C694163F1CA3"},"GWRE01":{"16:9 Widescreen":"AC42770B06662BE1DC863EC80F44B5E034C63664"},"GWRP01":{"16:9 Widescreen":"9DDDFAB28C4BD35CF64050E1EF684DC042B1AFFA"},"GWWE01":{"16:9 Widescreen":"98B2E75D8E1CED4A964D3129A5DC10E30538CAA6"},"GWWJ01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWWP01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWZE01":{"16:9 Widescreen":"9EFC191DE6D21A1681FE241AB2EE4A131259F317"},"GXCE01":{"16:9 Widescreen":"901A1E78A3A0124F55548507D3B3707125C64A8A"},"GXSE8P":{"16:9 Widescreen":"F50BBA440184FC77A4DFFAA58FF2BB888E2E072F","Aspect Ratio Fix":"4214C74DFB8A74FC3AA4A643E869BC4D9A38EDA0"},"GXSJ8P":{"Aspect Ratio Fix":"227909607984BBC3D36AAC7DB9DFE385F3363C49"},"GXXE01":{"16:9 Widescreen":"7CAFDAD7B5E7459CCC1BB209D439DDCBB5BE4E4E","60 FPS":"7E2829049003FC9DC8BC28044B33CAFF3768B54C","Allow Memory Card saving with Savestates":"64FAA15062F0D0C319F904BBDE9C4489A25D6369"},"GXXJ01":{"16:9 Widescreen":"AF704A8C6838FDF47054D9EDD9106B3F46781FA9","60 FPS":"4F5FD45B4EC1B33FE076FA06F8FF863C46A06BBA","Allow Memory Card saving with Savestates":"8293802260536FA2EF2EFDAB5266DE36BB88DE1B"},"GXXP01":{"16:9 Widescreen":"02716B2585BEE74F8FCDD97A78F6A0D3DC7F331B","60 FPS":"BF6171EA5CCF4B57AA596372589DDB5E6904DD7A","Allow Memory Card saving with Savestates":"3CAFBC4AE6FC5CE9F53377F86AB5BD8F1BC8861A"},"GYFPA4":{"60Hz":"402ED10AC842041AB4B39AE8F2D81B2D7AEF9CB4"},"GZ2E01":{"16:9 Widescreen v2":"E7521ED27BFC972628906CBE8D5403ED57253BB8","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2J01":{"16:9 Widescreen":"F985A0A58D8E2B23E8A557FAFF8D367AFFEADD07","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2P01":{"16:9 Widescreen v2":"CAC38B0D334B925A57AA3118D35932B8A185137E","Hyrule Field Speed Hack":"0F63623D4D984B7706F718F57C0ABDB6DBADCF8D"},"GZ3E70":{"16:9 Widescreen":"167F9E9A9B372CB3A01F308B46FF1403F8599C51","Deinterlacing Fix":"EF1291C0E45277CDAE4D0F61E1949C1E75E5A3E1"},"GZBJB2":{"Deinterlacing Fix":"19D67BD12A18F0E3A960BBB92E2F490BEF5C00C1"},"GZDP70":{"16:9 Widescreen":"2E424E0BBAE6EF5D6A8FB4224EA1D0746BAC37D3"},"GZLE01":{"16:9 Widescreen":"9FA864EE7DD8CE7FF538EB4E0243F20137430BD8","Remove Distance Blur":"78EA34CEF8E01701491C280F155F1C12EC9BF1A2"},"GZLJ01":{"16:9 Widescreen":"E03B61989025CA33937E63A057E6E40A403811BC"},"GZLP01":{"16:9 Widescreen":"3EFFF6C52B5633A1729FAA6883D8579E77F7D057","Remove Distance Blur":"14BECBEA4DD281EBD0F7FE7DEE8020B6F2418ACD"},"GZPE70":{"16:9 Widescreen":"7142F2495507AC7136992128ED0FCA6BC72B61F4"},"GZPP70":{"16:9 Widescreen":"591FD6C9668C79FF1CE8558EBED2486A7A327F05","60Hz":"A59B84DB5486521AEE1C23B6C741ECD35216E5AD"},"GZSE70":{"16:9 Widescreen":"8E86EAC7EA4F4D2854DD9020CD795630CA64C4EE"},"HAF":{"BufferPatch":"181195871F63B89B1CF09AFA4420CF89B9883108"},"HAL":{"RSAPatch":"AD12237401ABE9FE4A545AADB5C5AE10355E2076"},"P1RE01":{"60 FPS":"FD15A2EC38E46C1C3153336BC44F11B08ED906D0"},"R7XE69":{"Idle loop speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"R7XJ13":{"Idle loop speed hack":"4590D425BCBAFA0D1E32D27B46068843F6A2BE09"},"R7XP69":{"Idle loop speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"RB7E54":{"Limit internal frame rate in loading screens (fix hangs)":"BD1A252B43FD339391ED33554542E786BAD6671B"},"RB7P54":{"Limit internal frame rate in loading screens (fix hangs)":"90247F89DF8EB7A4AFA2C425CD3006257A95A8F8"},"RGQE70":{"crashfix":"5F4CF8D4DA19A0FF74FF9EB925AC0236069BFD59"},"RGWE41":{"Limit internal frame rate (speed hack)":"A0B4B457C42E3E2EE6D4E3E1F7E6EEAB2F70EBA5"},"RGWP41":{"Limit internal frame rate (speed hack)":"64A43222F25CA56BC44ED3FF80B555B16E6B9822"},"RGWX41r0":{"Limit internal frame rate (speed hack)":"64A43222F25CA56BC44ED3FF80B555B16E6B9822"},"RGWX41r1":{"Limit internal frame rate (speed hack)":"AFB612191047500D6BE2ACC7B4E96E6B72941B39"},"RJ2E52":{"Limit internal frame rate (speed hack)":"880DB9C1D639AA2ECD7AE6C96C93E8D83FBC38AF"},"RJ2JGD":{"Limit internal frame rate (speed hack)":"A387F6E3D973DCFD7D5A2E27183837358295B425"},"RJ2P52":{"Limit internal frame rate (speed hack)":"507BFC06E65A3C05D312305D44D1051A054003BF"},"RLEEFS":{"Fix crash on main menu":"793642AC6862C2F3412035A9E3D7172CC4A1D5C7"},"RM8E01":{"16:9 Widescreen":"E61344EB1542A78D497981C307B6549985C7A05A","Extra - Disable Music":"25223F9EFAABF601CAC7810004F124E4056598B1","QOL - Faster Boot Time":"BCC4279F8B28636AD773F01540E78DF40EAD6087","QOL - Increased Board Speed":"B65AF1819966CD3435D88801E8C79704E3A52DB5","QOL - Increased Taunt Capabilities":"66495D7CB532FAE778AFC22CF45D17D0FFDE5310","QOL - Increased Text Display":"B4186DDC54F33F4D6A22188EF50CEB43FB205673","QOL - Invert IR Stick for GameCube Mod":"9D90A9C66AE8AD91B201B40C4145D1323B701A77","QOL - Remove Explanations":"330DD53AB993A99576564FEFD222D7BD211B878F"},"RM8J01":{"16:9 Widescreen":"A140BDCB8E1721CB6B4CD878E412113322258B57"},"RM8P01":{"16:9 Widescreen":"F664C32AFD3D785FC6E04D8990A3FA1C72A18C5C"},"RMHE08":{"Bloom OFF":"CCF233DA57B3E75221870DE502955114B0D4E7FA"},"RMHJ08":{"Bloom OFF":"29D3625B7ED577587E56AA07CB0EB8C47C97E823"},"RMHP08":{"Bloom OFF":"1720C1173D4698167080DBFC4232F21757C4DA08"},"RO2P7N":{"Hangfix":"EEE9C8DE4671C18DD7F81DD08D39B64C57600DEA"},"RPBE01":{"Fix black screen effects":"775ABECA6073E02C5C68CF4D644194D966A418F5"},"RPBJ01r0":{"Fix black screen effects":"0EAB5D8DE827894AFEF97C10ACB67378E6983323"},"RPBJ01r1":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBJ01r2":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBP01":{"Fix black screen effects":"82AEB60F9A9083F93060531A970FFAABE0833A40"},"RQZ":{"Limit internal frame rate (speed hack)":"7B3B31BA970B319459C2E30B94E973CD1F2AE998"},"RRBE41r0":{"Idle loop speed hack":"20C7A2D4A3D6D55A8029A9D200D57CBD11F60B88"},"RRBE41r1":{"Idle loop speed hack":"0A30FA4C7E787197A00B7267368F372EFF623776"},"RRBE41r2":{"Idle loop speed hack":"2227787007F16F0CD454422AE6EB94656464B6D0"},"RRBJ41":{"Idle loop speed hack":"E9209EB728AA13375B2ABEF1C15D066DAFBF2EBF"},"RRBP41":{"Idle loop speed hack":"8A47BC311138EE3A11E049DF3EE6AED38990FB49"},"RTH":{"Disable blur":"812EE46AC967BFCD239335B10A664D71A93E8175"},"RUUE01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"B62652D5BBE0701900090CFEB32003AE6E5A5015","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"244D1FC7D6284DC8832CF7F34506B0DE4DBF8914","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"6FDD09D51EC3D754BA7F80630B1FE1D3FEF6FE97","QOL - Disable Grass Deterioration":"D566F2AF5BB1EDB8284461DEA85FF3B5A8DEBDD6","QOL - Enable Fast Text":"C7D8A7926ED952A2797DA312B41B08DCCDC3A274"},"RUUE01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"FC7BF1D6A5B4ED39EA4671C241E78F3BC8037E97","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"0235631B6F9A439B9718635F5AB7ADAFA2B6B3DE","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"5DEAC5F3390CE19487A9473A1988E7BB5EA4790F","QOL - Disable Grass Deterioration":"0FE1EF4DE5C8AE691E96634BC66BA034850C6C0F","QOL - Enable Fast Text":"B2BFF779434FDBEF08FFD73C4B140FCE0A8FDF0E"},"RUUJ01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"D79F773E1363B694784C94DD309709D41D368A08","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"C88CFD7E6C18F570DE974D6C0202987E4F1B72C4","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"3C92010E7DE2E93C48AFF25B30A18BBFCBD12707","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUK01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"1B89E8A6E6E09757373FF116BBC0989C06651709","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"EF883FB0C237BF51ABD88C35D34571D240C79484","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"F08D0F233F3F5833DFAFE5C8049C37D32360F0FA","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r0":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"C55A5CDEF4C868E66C1FB2902D3285B2C350D7F1","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"D28D4E4FBD153A6D4C49E2D250213F37BEC941E5","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"80C3F50C018E076DF2CD39732A5ABB8A1C0E88B1","QOL - Disable Grass Deterioration":"3B278C19AC76D27488C55201BA0DB63864F66AAF","QOL - Enable Fast Text":"EFC4517A0915A19F1EC93BFBF91C501124857C18"},"RUUP01r1":{"QOL - Classic Controller Support (ACCF: Deluxe) v1.2":"00D994940AD323786A9DA780D1347AE5FBABAC8A","QOL - Classic Controller Support (GBAtemp - Left Stick Cursor) v1.1":"9121BEDD72436F4EAB47E4CE96ABD864118B22F3","QOL - Classic Controller Support (GBAtemp - Right Stick Cursor) v1.1":"8FC58B3DC4817515822EBB82BB7A8055270EE51D","QOL - Disable Grass Deterioration":"9B199D77AF748518358D32966F3F55F5C5759C3A","QOL - Enable Fast Text":"827CF5DFC90A258C36BF32DC3883371973E0B706"},"RX4E4Z":{"Fix file reads (dcache bypass)":"9E4E0F1465A9A1E85349DBA3B1278AC215A97DBB"},"RX4PMT":{"Fix file reads (dcache bypass)":"EE85907C03F0295794821383B93F8D5B91D2697A"},"RZDE01r0":{"Hyrule Field Speed Hack":"15EAD073414C9903D6CAE5229DCE582BD17A9162"},"RZDE01r2":{"Hyrule Field Speed Hack":"27395CC8BC2C51201D566657D31A471A850482FB"},"RZDJ01":{"Hyrule Field Speed Hack":"B3F7473F8C911A32F1D616491C9E78EBBD7A6309"},"RZDK01":{"Hyrule Field Speed Hack":"A280C0114B800D7DC056ECFB5E482229DA0B1550"},"RZDP01":{"Hyrule Field Speed Hack":"2A83ADFB760F9498841ED0ED68B0C0438232472C"},"SAOE78":{"Fix crash on boot":"EA11FA4908FB20B61876ACD360EC7657A6D39FB2"},"SAOEVZ":{"Fix crash on boot":"AA55C214DE7545DE0E203CC39F06BF3D31451BE9"},"SC2":{"Limit internal frame rate (speed hack)":"B3FB0FCE108E85E6198BAF3535850470049E2BE2"},"SCYE4Q":{"BAT Speedhack":"D702C348A02C6778BAB5851210A258E9AD53F8F5"},"SCYP4Q":{"BAT Speedhack":"D702C348A02C6778BAB5851210A258E9AD53F8F5"},"SCYR4Q":{"BAT Speedhack":"F90BDE3B7892FD161384FBC3CA72D05F8B6EB43C"},"SCYX4Q":{"BAT Speedhack":"91BAE02F25F4A12582800CDB93D213419CB146C0"},"SCYY4Q":{"BAT Speedhack":"CBD80E291536141196823032D867E002CFE8AA16"},"SCYZ4Q":{"BAT Speedhack":"CBD80E291536141196823032D867E002CFE8AA16"},"SDVE41":{"Limit internal frame rate (speed hack)":"5F83B73ADB49CC1E0AB30AEB4E5E64197B21F444"},"SDVP41":{"Limit internal frame rate (speed hack)":"6F282D2EBE6F5574516052FCB5B94D612C59B6D9"},"SGLEA4":{"Fix black screen":"258378187ACF475A55EFEAF8A703681252E014C3"},"SGLPA4":{"Fix black screen":"6F8CD59D897338CA90939149E1A62588620C6D88"},"SQIE4Q":{"BAT Speedhack":"37009D83FCAE2E26E93CB9AB2A99BACBECAE5010"},"SQIP4Q":{"BAT Speedhack":"37009D83FCAE2E26E93CB9AB2A99BACBECAE5010"},"SQIY4Q":{"BAT Speedhack":"37009D83FCAE2E26E93CB9AB2A99BACBECAE5010"},"STSE4Q":{"BAT Speedhack":"259F13AB4489F5B6EFD48F1D1F4A1FFDEFD6BB48"},"STSP4Qr1":{"BAT Speedhack":"259F13AB4489F5B6EFD48F1D1F4A1FFDEFD6BB48"},"STSP4Qr2":{"BAT Speedhack":"31A4FA663DD548275EE4D7241E51E3AA30B27136"},"STSX4Q":{"BAT Speedhack":"31A4FA663DD548275EE4D7241E51E3AA30B27136"},"STSY4Qr0":{"BAT Speedhack":"259F13AB4489F5B6EFD48F1D1F4A1FFDEFD6BB48"},"STSY4Qr1":{"BAT Speedhack":"31A4FA663DD548275EE4D7241E51E3AA30B27136"},"STSZ4Q":{"BAT Speedhack":"259F13AB4489F5B6EFD48F1D1F4A1FFDEFD6BB48"},"SX4E01":{"60 FPS":"206E79C5E9B5E07CEB7F52D0F5E5FF3400009265"},"WR2E41":{"Limit internal frame rate (speed hack)":"169567FB65FB2CCB0210213C6DAD65E9937AB9DB"},"WR2P41":{"Limit internal frame rate (speed hack)":"0F70B210236BE666004AB5681151242C22496DBB"}} \ No newline at end of file diff --git a/Data/Sys/GameSettings/GDSE78.ini b/Data/Sys/GameSettings/GDSE78.ini index 5982fb3bb90b..ec1ea720c34b 100644 --- a/Data/Sys/GameSettings/GDSE78.ini +++ b/Data/Sys/GameSettings/GDSE78.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GDSP78.ini b/Data/Sys/GameSettings/GDSP78.ini index 23c786c55c1a..4dc777b1bec7 100644 --- a/Data/Sys/GameSettings/GDSP78.ini +++ b/Data/Sys/GameSettings/GDSP78.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GE4.ini b/Data/Sys/GameSettings/GE4.ini index 38aacca65d73..4db0f8462b91 100644 --- a/Data/Sys/GameSettings/GE4.ini +++ b/Data/Sys/GameSettings/GE4.ini @@ -8,11 +8,14 @@ SyncOnSkipIdle = False [OnFrame] # `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x80013818:dword:0x60000000 [ActionReplay] # Add action replay cheats here. [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GH2E69.ini b/Data/Sys/GameSettings/GH2E69.ini index 743c2a4f2ad9..ed245193c3b7 100644 --- a/Data/Sys/GameSettings/GH2E69.ini +++ b/Data/Sys/GameSettings/GH2E69.ini @@ -10,3 +10,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GH2P69.ini b/Data/Sys/GameSettings/GH2P69.ini index 0376b45c7f5b..bd453f7d621e 100644 --- a/Data/Sys/GameSettings/GH2P69.ini +++ b/Data/Sys/GameSettings/GH2P69.ini @@ -10,3 +10,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GHQP7D.ini b/Data/Sys/GameSettings/GHQP7D.ini index 0beb2cd42ee9..eb9f3dcf2d9f 100644 --- a/Data/Sys/GameSettings/GHQP7D.ini +++ b/Data/Sys/GameSettings/GHQP7D.ini @@ -5,6 +5,9 @@ $Limit internal frame rate during startup (speed hack) 0x80277034:dword:0x60000000 0x80277040:dword:0x60000000 +[OnFrame_Enabled] +$Limit internal frame rate during startup (speed hack) + [Gecko] $16:9 Widescreen 04004600 C002A604 diff --git a/Data/Sys/GameSettings/GMNE78.ini b/Data/Sys/GameSettings/GMNE78.ini index eff3d832fb3c..39560ec083f3 100644 --- a/Data/Sys/GameSettings/GMNE78.ini +++ b/Data/Sys/GameSettings/GMNE78.ini @@ -2,11 +2,11 @@ [OnFrame] # After `GXDrawDone`, `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x80149C38:dword:0x60000000 [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) [Patches_RetroAchievements_Verified] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GMNP78.ini b/Data/Sys/GameSettings/GMNP78.ini index 62d2e01716af..11b20f8134a2 100644 --- a/Data/Sys/GameSettings/GMNP78.ini +++ b/Data/Sys/GameSettings/GMNP78.ini @@ -2,11 +2,11 @@ [OnFrame] # After `GXDrawDone`, `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x8014CF88:dword:0x60000000 [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) [Patches_RetroAchievements_Verified] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GTRE78r0.ini b/Data/Sys/GameSettings/GTRE78r0.ini index 1adeeea7b3a7..e504bdee600e 100644 --- a/Data/Sys/GameSettings/GTRE78r0.ini +++ b/Data/Sys/GameSettings/GTRE78r0.ini @@ -2,11 +2,11 @@ [OnFrame] # After `GXDrawDone`, `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x80101A80:dword:0x60000000 [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) [Patches_RetroAchievements_Verified] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GTRE78r1.ini b/Data/Sys/GameSettings/GTRE78r1.ini index ae762dacefc3..830a47fde6b9 100644 --- a/Data/Sys/GameSettings/GTRE78r1.ini +++ b/Data/Sys/GameSettings/GTRE78r1.ini @@ -2,11 +2,11 @@ [OnFrame] # After `GXDrawDone`, `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x80101B08:dword:0x60000000 [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) [Patches_RetroAchievements_Verified] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GTRJ8N.ini b/Data/Sys/GameSettings/GTRJ8N.ini index ea99bedfa7a2..9890b61629c5 100644 --- a/Data/Sys/GameSettings/GTRJ8N.ini +++ b/Data/Sys/GameSettings/GTRJ8N.ini @@ -2,11 +2,11 @@ [OnFrame] # After `GXDrawDone`, `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x8010F210:dword:0x60000000 [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) [Patches_RetroAchievements_Verified] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/GTRP78.ini b/Data/Sys/GameSettings/GTRP78.ini index e5184bd15c6c..1df45443b6bd 100644 --- a/Data/Sys/GameSettings/GTRP78.ini +++ b/Data/Sys/GameSettings/GTRP78.ini @@ -2,11 +2,11 @@ [OnFrame] # After `GXDrawDone`, `nop` a branch that skips a call to `VIWaitForRetrace`. -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) 0x80101390:dword:0x60000000 [OnFrame_Enabled] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) [Patches_RetroAchievements_Verified] -$Limit internal FPS to VPS (improves performance) +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/R7XE69.ini b/Data/Sys/GameSettings/R7XE69.ini index 52042d2e2c7e..d31ea6d60118 100644 --- a/Data/Sys/GameSettings/R7XE69.ini +++ b/Data/Sys/GameSettings/R7XE69.ini @@ -6,11 +6,11 @@ # The loop waits for a vblank interrupt, spamming `OSYieldThread` in the meantime. # The patch replaces the call to `VIWaitForRetrace`, which still executes the other threads, # but sleeps instead of spinlocking when there is no thread to execute. -$Speed hack +$Idle loop speed hack 0x802B2908:dword:0x4826F5C9 [OnFrame_Enabled] -$Speed hack +$Idle loop speed hack [Patches_RetroAchievements_Verified] -$Speed hack +$Idle loop speed hack diff --git a/Data/Sys/GameSettings/R7XJ13.ini b/Data/Sys/GameSettings/R7XJ13.ini index 89de3a33ad91..15574ab68331 100644 --- a/Data/Sys/GameSettings/R7XJ13.ini +++ b/Data/Sys/GameSettings/R7XJ13.ini @@ -6,11 +6,11 @@ # The loop waits for a vblank interrupt, spamming `OSYieldThread` in the meantime. # The patch replaces the call to `VIWaitForRetrace`, which still executes the other threads, # but sleeps instead of spinlocking when there is no thread to execute. -$Speed hack +$Idle loop speed hack 0x802B3118:dword:0x4826F5B9 [OnFrame_Enabled] -$Speed hack +$Idle loop speed hack [Patches_RetroAchievements_Verified] -$Speed hack +$Idle loop speed hack diff --git a/Data/Sys/GameSettings/R7XP69.ini b/Data/Sys/GameSettings/R7XP69.ini index 58d64d8ef3c6..502def215c35 100644 --- a/Data/Sys/GameSettings/R7XP69.ini +++ b/Data/Sys/GameSettings/R7XP69.ini @@ -6,11 +6,11 @@ # The loop waits for a vblank interrupt, spamming `OSYieldThread` in the meantime. # The patch replaces the call to `VIWaitForRetrace`, which still executes the other threads, # but sleeps instead of spinlocking when there is no thread to execute. -$Speed hack +$Idle loop speed hack 0x802B2908:dword:0x4826F5C9 [OnFrame_Enabled] -$Speed hack +$Idle loop speed hack [Patches_RetroAchievements_Verified] -$Speed hack +$Idle loop speed hack diff --git a/Data/Sys/GameSettings/RGWE41.ini b/Data/Sys/GameSettings/RGWE41.ini index 4e927fcd0ca2..31269a7f42fc 100644 --- a/Data/Sys/GameSettings/RGWE41.ini +++ b/Data/Sys/GameSettings/RGWE41.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RGWP41.ini b/Data/Sys/GameSettings/RGWP41.ini index fc96a8dc8bf0..9f3c7f87c846 100644 --- a/Data/Sys/GameSettings/RGWP41.ini +++ b/Data/Sys/GameSettings/RGWP41.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RGWX41r0.ini b/Data/Sys/GameSettings/RGWX41r0.ini index 9ec023ef1277..d3ffc9ee526a 100644 --- a/Data/Sys/GameSettings/RGWX41r0.ini +++ b/Data/Sys/GameSettings/RGWX41r0.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RGWX41r1.ini b/Data/Sys/GameSettings/RGWX41r1.ini index 0ff75e614bde..d6ebdc80bb1a 100644 --- a/Data/Sys/GameSettings/RGWX41r1.ini +++ b/Data/Sys/GameSettings/RGWX41r1.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RJ2E52.ini b/Data/Sys/GameSettings/RJ2E52.ini index 8255e17be95a..e6d6aa5d58e9 100644 --- a/Data/Sys/GameSettings/RJ2E52.ini +++ b/Data/Sys/GameSettings/RJ2E52.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RJ2JGD.ini b/Data/Sys/GameSettings/RJ2JGD.ini index 08530713b708..a1642fc24aa3 100644 --- a/Data/Sys/GameSettings/RJ2JGD.ini +++ b/Data/Sys/GameSettings/RJ2JGD.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RJ2P52.ini b/Data/Sys/GameSettings/RJ2P52.ini index 2476cd18c8af..6390ee5d8b3d 100644 --- a/Data/Sys/GameSettings/RJ2P52.ini +++ b/Data/Sys/GameSettings/RJ2P52.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/RQZ.ini b/Data/Sys/GameSettings/RQZ.ini index e9dbfb06088b..c1c79b25341a 100644 --- a/Data/Sys/GameSettings/RQZ.ini +++ b/Data/Sys/GameSettings/RQZ.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/SCYE4Q.ini b/Data/Sys/GameSettings/SCYE4Q.ini index db2beb6c8d24..2a1f63372f29 100644 --- a/Data/Sys/GameSettings/SCYE4Q.ini +++ b/Data/Sys/GameSettings/SCYE4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SCYP4Q.ini b/Data/Sys/GameSettings/SCYP4Q.ini index f6d6099a2293..0e04a9345cd6 100644 --- a/Data/Sys/GameSettings/SCYP4Q.ini +++ b/Data/Sys/GameSettings/SCYP4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SCYR4Q.ini b/Data/Sys/GameSettings/SCYR4Q.ini index 274f4326a2b2..304d3cba0440 100644 --- a/Data/Sys/GameSettings/SCYR4Q.ini +++ b/Data/Sys/GameSettings/SCYR4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SCYX4Q.ini b/Data/Sys/GameSettings/SCYX4Q.ini index fe8ab5cbad30..cd31d198011c 100644 --- a/Data/Sys/GameSettings/SCYX4Q.ini +++ b/Data/Sys/GameSettings/SCYX4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SCYY4Q.ini b/Data/Sys/GameSettings/SCYY4Q.ini index 8deb48f27d17..a09aba585449 100644 --- a/Data/Sys/GameSettings/SCYY4Q.ini +++ b/Data/Sys/GameSettings/SCYY4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SCYZ4Q.ini b/Data/Sys/GameSettings/SCYZ4Q.ini index d0a855b07ae7..4c173e126b5b 100644 --- a/Data/Sys/GameSettings/SCYZ4Q.ini +++ b/Data/Sys/GameSettings/SCYZ4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SDVE41.ini b/Data/Sys/GameSettings/SDVE41.ini index 2ba3036de037..2d1fbde5de1b 100644 --- a/Data/Sys/GameSettings/SDVE41.ini +++ b/Data/Sys/GameSettings/SDVE41.ini @@ -7,3 +7,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/SDVP41.ini b/Data/Sys/GameSettings/SDVP41.ini index 2df36f893ac9..b22b9dfb1a37 100644 --- a/Data/Sys/GameSettings/SDVP41.ini +++ b/Data/Sys/GameSettings/SDVP41.ini @@ -7,3 +7,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/SQIE4Q.ini b/Data/Sys/GameSettings/SQIE4Q.ini index a3c8c76eb5ed..da4d90e146e7 100644 --- a/Data/Sys/GameSettings/SQIE4Q.ini +++ b/Data/Sys/GameSettings/SQIE4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SQIP4Q.ini b/Data/Sys/GameSettings/SQIP4Q.ini index c06cd79d8bba..a5a612aa73dc 100644 --- a/Data/Sys/GameSettings/SQIP4Q.ini +++ b/Data/Sys/GameSettings/SQIP4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/SQIY4Q.ini b/Data/Sys/GameSettings/SQIY4Q.ini index dfc699ec89e2..026003aa0808 100644 --- a/Data/Sys/GameSettings/SQIY4Q.ini +++ b/Data/Sys/GameSettings/SQIY4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSE4Q.ini b/Data/Sys/GameSettings/STSE4Q.ini index 63b6d5391d1e..cb9bfe9bd1a1 100644 --- a/Data/Sys/GameSettings/STSE4Q.ini +++ b/Data/Sys/GameSettings/STSE4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSP4Qr1.ini b/Data/Sys/GameSettings/STSP4Qr1.ini index 8e8b80e4f366..b5bb3a8634b0 100644 --- a/Data/Sys/GameSettings/STSP4Qr1.ini +++ b/Data/Sys/GameSettings/STSP4Qr1.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSP4Qr2.ini b/Data/Sys/GameSettings/STSP4Qr2.ini index a2b156c6d107..5779f4975a8f 100644 --- a/Data/Sys/GameSettings/STSP4Qr2.ini +++ b/Data/Sys/GameSettings/STSP4Qr2.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSX4Q.ini b/Data/Sys/GameSettings/STSX4Q.ini index 73dc8c8540ef..c21c76c27b2a 100644 --- a/Data/Sys/GameSettings/STSX4Q.ini +++ b/Data/Sys/GameSettings/STSX4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSY4Qr0.ini b/Data/Sys/GameSettings/STSY4Qr0.ini index 2d2880041ec2..e4d2fe6fbe2d 100644 --- a/Data/Sys/GameSettings/STSY4Qr0.ini +++ b/Data/Sys/GameSettings/STSY4Qr0.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSY4Qr1.ini b/Data/Sys/GameSettings/STSY4Qr1.ini index 4944a4b9263a..df7b517c8cb1 100644 --- a/Data/Sys/GameSettings/STSY4Qr1.ini +++ b/Data/Sys/GameSettings/STSY4Qr1.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/STSZ4Q.ini b/Data/Sys/GameSettings/STSZ4Q.ini index 67415ce6b8c2..e5b8664f527e 100644 --- a/Data/Sys/GameSettings/STSZ4Q.ini +++ b/Data/Sys/GameSettings/STSZ4Q.ini @@ -7,3 +7,6 @@ $BAT Speedhack [OnFrame_Enabled] $BAT Speedhack + +[Patches_RetroAchievements_Verified] +$BAT Speedhack diff --git a/Data/Sys/GameSettings/WR2E41.ini b/Data/Sys/GameSettings/WR2E41.ini index 219fa5157c83..05745d70824f 100644 --- a/Data/Sys/GameSettings/WR2E41.ini +++ b/Data/Sys/GameSettings/WR2E41.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Data/Sys/GameSettings/WR2P41.ini b/Data/Sys/GameSettings/WR2P41.ini index 4336327bcb3e..800add7d7bc4 100644 --- a/Data/Sys/GameSettings/WR2P41.ini +++ b/Data/Sys/GameSettings/WR2P41.ini @@ -6,3 +6,6 @@ $Limit internal frame rate (speed hack) [OnFrame_Enabled] $Limit internal frame rate (speed hack) + +[Patches_RetroAchievements_Verified] +$Limit internal frame rate (speed hack) diff --git a/Source/Core/Core/AchievementApprovedHash.h b/Source/Core/Core/AchievementApprovedHash.h index ff137737a32d..d7962ebd3ce0 100644 --- a/Source/Core/Core/AchievementApprovedHash.h +++ b/Source/Core/Core/AchievementApprovedHash.h @@ -9,5 +9,5 @@ static constexpr std::string_view ACHIEVEMENT_APPROVED_LIST_FILENAME = "Approved // After building tests, find the new hash with: // ./Binaries/Tests/tests --gtest_filter=PatchAllowlist.VerifyHashes static const inline Common::SHA1::Digest ACHIEVEMENT_APPROVED_LIST_HASH = { - 0x0B, 0x47, 0x3F, 0x5A, 0x84, 0xC0, 0xC1, 0x6D, 0x08, 0x88, - 0x05, 0x5C, 0xC6, 0x17, 0x60, 0xED, 0x02, 0x3C, 0x75, 0xF0}; + 0xE6, 0xCD, 0xD7, 0x85, 0x7A, 0xBA, 0x72, 0xEC, 0x34, 0x11, + 0x2B, 0x16, 0xB1, 0x31, 0xD0, 0x0A, 0x0A, 0xD7, 0xFC, 0xCC}; From 424269ed4b0fa650c3798ffb4b5c240d51b14b9f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 5 Jul 2026 10:38:44 +0200 Subject: [PATCH 111/270] Android: Silence deprecation warnings in ControllerInterface We have to use the deprecated APIs when running on old devices. --- .../dolphinemu/features/input/model/ControllerInterface.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt index ceba48e4bf1a..b412b6ef9b13 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt @@ -160,6 +160,7 @@ object ControllerInterface { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { DolphinVibratorManagerPassthrough(device.vibratorManager) } else { + @Suppress("DEPRECATION") DolphinVibratorManagerCompat(device.vibrator) } } @@ -174,6 +175,7 @@ object ControllerInterface { return DolphinVibratorManagerPassthrough(vibratorManager) } } + @Suppress("DEPRECATION") val vibrator = DolphinApplication.getAppContext() .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator return DolphinVibratorManagerCompat(vibrator) @@ -190,11 +192,13 @@ object ControllerInterface { } else { val attributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + @Suppress("DEPRECATION") vibrator.vibrate( VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE), attributes ) } else { + @Suppress("DEPRECATION") vibrator.vibrate(100, attributes) } } From b67188cc442d4b2d38cfb95b83cbeb0525ca257e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 27 Jun 2026 08:43:25 +0200 Subject: [PATCH 112/270] Android: Make vibration length variable Previously we would vibrate for 100 ms on every vibration, which was especially annoying if a game was doing a bunch of vibrations that were supposed to be much shorter than that. Now we tell Android to vibrate for 10 s, then cancel the vibration as soon as the game turns the vibration off. --- .../features/input/model/ControllerInterface.kt | 12 +++++++++--- .../ControllerInterface/Android/Android.cpp | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt index b412b6ef9b13..bcd47781a0c4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt @@ -186,7 +186,7 @@ object ControllerInterface { private fun vibrate(vibrator: Vibrator) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { vibrator.vibrate( - VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE), + VibrationEffect.createOneShot(10000, VibrationEffect.DEFAULT_AMPLITUDE), VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_MEDIA).build() ) } else { @@ -194,16 +194,22 @@ object ControllerInterface { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @Suppress("DEPRECATION") vibrator.vibrate( - VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE), + VibrationEffect.createOneShot(10000, VibrationEffect.DEFAULT_AMPLITUDE), attributes ) } else { @Suppress("DEPRECATION") - vibrator.vibrate(100, attributes) + vibrator.vibrate(10000, attributes) } } } + @Keep + @JvmStatic + private fun cancelVibration(vibrator: Vibrator) { + vibrator.cancel() + } + private class InputDeviceListener : InputManager.InputDeviceListener { override external fun onInputDeviceAdded(deviceId: Int) diff --git a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp index 40f1e2b09669..1aa5275f30a8 100644 --- a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp @@ -69,6 +69,7 @@ jmethodID s_controller_interface_unregister_input_device_listener; jmethodID s_controller_interface_get_vibrator_manager; jmethodID s_controller_interface_get_system_vibrator_manager; jmethodID s_controller_interface_vibrate; +jmethodID s_controller_interface_cancel_vibration; jclass s_sensor_event_listener_class; jmethodID s_sensor_event_listener_constructor; @@ -602,6 +603,11 @@ class AndroidMotor : public Core::Device::Output IDCache::GetEnvForThread()->CallStaticVoidMethod(s_controller_interface_class, s_controller_interface_vibrate, m_vibrator); } + else if (old_state >= 0.5 && state < 0.5) + { + IDCache::GetEnvForThread()->CallStaticVoidMethod( + s_controller_interface_class, s_controller_interface_cancel_vibration, m_vibrator); + } } private: @@ -892,6 +898,8 @@ InputBackend::InputBackend(ControllerInterface* controller_interface) "()Lorg/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager;"); s_controller_interface_vibrate = env->GetStaticMethodID(s_controller_interface_class, "vibrate", "(Landroid/os/Vibrator;)V"); + s_controller_interface_cancel_vibration = env->GetStaticMethodID( + s_controller_interface_class, "cancelVibration", "(Landroid/os/Vibrator;)V"); env->DeleteLocalRef(controller_interface_class); const jclass sensor_event_listener_class = From 82e576a1839306881204948480b1946e73fb1e7b Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 27 Jun 2026 08:45:44 +0200 Subject: [PATCH 113/270] Core: Remove ifdef for call to GCAdapter::ResetRumble The ifdef made it so this was never called on Android, but we do want it to be called on Android. There are ifdefs inside the function implementation but not around the function declaration or definition, so there's no need for ifdefs when calling it. --- Source/Core/Core/Core.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 1b50796e34e5..8ce66e67bb2f 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -252,9 +252,7 @@ bool Init(Core::System& system, std::unique_ptr boot, const Wind static void ResetRumble() { -#if defined(__LIBUSB__) GCAdapter::ResetRumble(); -#endif if (!Pad::IsInitialized()) return; for (int i = 0; i < 4; ++i) From c93af1d3f94deed81cf288f73c3cfd90aae8e500 Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Thu, 30 Apr 2026 16:28:38 -0400 Subject: [PATCH 114/270] Android: Convert ContentHandler to Kotlin and fix warnings --- .../dolphinemu/utils/ContentHandler.java | 425 ------------------ .../dolphinemu/utils/ContentHandler.kt | 331 ++++++++++++++ 2 files changed, 331 insertions(+), 425 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java deleted file mode 100644 index 9143375f769f..000000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java +++ /dev/null @@ -1,425 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.utils; - -import android.content.ContentResolver; -import android.database.Cursor; -import android.net.Uri; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.dolphinemu.dolphinemu.DolphinApplication; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Predicate; - -/* - We use a lot of "catch (Exception e)" in this class. This is for two reasons: - - 1. We don't want any exceptions to escape to native code, as this leads to nasty crashes - that often don't have stack traces that make sense. - - 2. The sheer number of different exceptions, both documented and undocumented. These include: - - FileNotFoundException when a file doesn't exist - - FileNotFoundException when using an invalid open mode (according to the documentation) - - IllegalArgumentException when using an invalid open mode (in practice with FileProvider) - - IllegalArgumentException when providing a tree where a document was expected and vice versa - - SecurityException when trying to access something the user hasn't granted us permission to - - UnsupportedOperationException when a URI specifies a storage provider that doesn't exist - */ - -public class ContentHandler -{ - public static boolean isContentUri(@NonNull String pathOrUri) - { - return pathOrUri.startsWith("content://"); - } - - @Keep - public static int openFd(@NonNull String uri, @NonNull String mode) - { - try - { - return getContentResolver().openFileDescriptor(unmangle(uri), mode).detachFd(); - } - catch (SecurityException e) - { - Log.error("Tried to open " + uri + " without permission"); - } - catch (Exception ignored) - { - } - - return -1; - } - - @Keep - public static boolean delete(@NonNull String uri) - { - try - { - return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri)); - } - catch (FileNotFoundException e) - { - // Return true because we care about the file not being there, not the actual delete. - return true; - } - catch (SecurityException e) - { - Log.error("Tried to delete " + uri + " without permission"); - } - catch (Exception ignored) - { - } - - return false; - } - - public static boolean exists(@NonNull String uri) - { - try - { - Uri documentUri = treeToDocument(unmangle(uri)); - final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; - try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) - { - return cursor != null && cursor.getCount() > 0; - } - } - catch (SecurityException e) - { - Log.error("Tried to check if " + uri + " exists without permission"); - } - catch (Exception ignored) - { - } - - return false; - } - - /** - * @return -1 if not found, -2 if directory, file size otherwise - */ - @Keep - public static long getSizeAndIsDirectory(@NonNull String uri) - { - try - { - Uri documentUri = treeToDocument(unmangle(uri)); - final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; - try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) - { - if (cursor != null && cursor.moveToFirst()) - { - if (Document.MIME_TYPE_DIR.equals(cursor.getString(0))) - return -2; - else - return cursor.isNull(1) ? 0 : cursor.getLong(1); - } - } - } - catch (SecurityException e) - { - Log.error("Tried to get metadata for " + uri + " without permission"); - } - catch (Exception ignored) - { - } - - return -1; - } - - @Nullable @Keep - public static String getDisplayName(@NonNull String uri) - { - try - { - return getDisplayName(unmangle(uri)); - } - catch (Exception ignored) - { - } - - return null; - } - - @Nullable - public static String getDisplayName(@NonNull Uri uri) - { - final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME}; - Uri documentUri = treeToDocument(uri); - try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) - { - if (cursor != null && cursor.moveToFirst()) - { - return cursor.getString(0); - } - } - catch (SecurityException e) - { - Log.error("Tried to get display name of " + uri + " without permission"); - } - catch (Exception ignored) - { - } - - return null; - } - - @NonNull @Keep - public static String[] getChildNames(@NonNull String uri, boolean recursive) - { - try - { - return getChildNames(unmangle(uri), recursive); - } - catch (Exception ignored) - { - } - - return new String[0]; - } - - @NonNull - public static String[] getChildNames(@NonNull Uri uri, boolean recursive) - { - ArrayList result = new ArrayList<>(); - - ForEachChildCallback callback = new ForEachChildCallback() - { - @Override - public void run(String displayName, String documentId, boolean isDirectory) - { - if (recursive && isDirectory) - { - forEachChild(uri, documentId, this); - } - else - { - result.add(displayName); - } - } - }; - - forEachChild(uri, DocumentsContract.getDocumentId(treeToDocument(uri)), callback); - - return result.toArray(new String[0]); - } - - @NonNull @Keep - public static String[] doFileSearch(@NonNull String directory, @NonNull String[] extensions, - boolean recursive) - { - ArrayList result = new ArrayList<>(); - - try - { - Uri uri = unmangle(directory); - String documentId = DocumentsContract.getDocumentId(treeToDocument(uri)); - boolean acceptAll = extensions.length == 0; - Predicate extensionCheck = (displayName) -> - { - String extension = FileBrowserHelper.getExtension(displayName, true); - return extension != null && Arrays.stream(extensions).anyMatch(extension::equalsIgnoreCase); - }; - doFileSearch(uri, directory, documentId, recursive, result, acceptAll, extensionCheck); - } - catch (Exception ignored) - { - } - - return result.toArray(new String[0]); - } - - private static void doFileSearch(@NonNull Uri baseUri, @NonNull String path, - @NonNull String documentId, boolean recursive, @NonNull List resultOut, - boolean acceptAll, @NonNull Predicate extensionCheck) - { - forEachChild(baseUri, documentId, (displayName, childDocumentId, isDirectory) -> - { - String childPath = path + '/' + displayName; - if (acceptAll || (!isDirectory && extensionCheck.test(displayName))) - { - resultOut.add(childPath); - } - if (recursive && isDirectory) - { - doFileSearch(baseUri, childPath, childDocumentId, recursive, resultOut, acceptAll, - extensionCheck); - } - }); - } - - private interface ForEachChildCallback - { - void run(String displayName, String documentId, boolean isDirectory); - } - - private static void forEachChild(@NonNull Uri uri, @NonNull String documentId, - @NonNull ForEachChildCallback callback) - { - try - { - Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId); - - final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, Document.COLUMN_DOCUMENT_ID}; - try (Cursor cursor = getContentResolver().query(childrenUri, projection, null, null, null)) - { - if (cursor != null) - { - while (cursor.moveToNext()) - { - callback.run(cursor.getString(0), cursor.getString(2), - Document.MIME_TYPE_DIR.equals(cursor.getString(1))); - } - } - } - } - catch (SecurityException e) - { - Log.error("Tried to get children of " + uri + " without permission"); - } - catch (Exception ignored) - { - } - } - - @NonNull - private static Uri getChild(@NonNull Uri parentUri, @NonNull String childName) - throws FileNotFoundException, SecurityException - { - String parentId = DocumentsContract.getDocumentId(treeToDocument(parentUri)); - Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(parentUri, parentId); - - final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_DOCUMENT_ID}; - final String selection = Document.COLUMN_DISPLAY_NAME + "=?"; - final String[] selectionArgs = new String[]{childName}; - try (Cursor cursor = getContentResolver().query(childrenUri, projection, selection, - selectionArgs, null)) - { - if (cursor != null) - { - while (cursor.moveToNext()) - { - // FileProvider seemingly doesn't support selections, so we have to manually filter here - if (childName.equals(cursor.getString(0))) - { - return DocumentsContract.buildDocumentUriUsingTree(parentUri, cursor.getString(1)); - } - } - } - } - catch (SecurityException e) - { - Log.error("Tried to get child " + childName + " of " + parentUri + " without permission"); - } - catch (Exception ignored) - { - } - - throw new FileNotFoundException(parentUri + "/" + childName); - } - - /** - * Since our C++ code was written under the assumption that it would be running under a filesystem - * which supports normal paths, it appends a slash followed by a file name when it wants to access - * a file in a directory. This function translates that into the type of URI that SAF requires. - * - * In order to detect whether a URI is mangled or not, we make the assumption that an - * unmangled URI contains at least one % and does not contain any slashes after the last %. - * This seems to hold for all common storage providers, but it is theoretically for a storage - * provider to use URIs without any % characters. - */ - @NonNull - public static Uri unmangle(@NonNull String uri) throws FileNotFoundException, SecurityException - { - int lastComponentEnd = getLastComponentEnd(uri); - int lastComponentStart = getLastComponentStart(uri, lastComponentEnd); - - if (lastComponentStart == 0) - { - return Uri.parse(uri.substring(0, lastComponentEnd)); - } - else - { - Uri parentUri = unmangle(uri.substring(0, lastComponentStart)); - String childName = uri.substring(lastComponentStart, lastComponentEnd); - return getChild(parentUri, childName); - } - } - - /** - * Returns the last character which is not a slash. - */ - private static int getLastComponentEnd(@NonNull String uri) - { - int i = uri.length(); - while (i > 0 && uri.charAt(i - 1) == '/') - i--; - return i; - } - - /** - * Scans backwards starting from lastComponentEnd and returns the index after the first slash - * it finds, but only if there is a % before that slash and there is no % after it. - */ - private static int getLastComponentStart(@NonNull String uri, int lastComponentEnd) - { - int i = lastComponentEnd; - while (i > 0 && uri.charAt(i - 1) != '/') - { - i--; - if (uri.charAt(i) == '%') - return 0; - } - - int j = i; - while (j > 0) - { - j--; - if (uri.charAt(j) == '%') - return i; - } - - return 0; - } - - @NonNull - private static Uri treeToDocument(@NonNull Uri uri) - { - if (isTreeUri(uri)) - { - String documentId = DocumentsContract.getTreeDocumentId(uri); - return DocumentsContract.buildDocumentUriUsingTree(uri, documentId); - } - else - { - return uri; - } - } - - /** - * This is like DocumentsContract.isTreeUri, except it doesn't return true for URIs like - * content://com.example/tree/12/document/24/. We want to treat those as documents, not trees. - */ - private static boolean isTreeUri(@NonNull Uri uri) - { - final List pathSegments = uri.getPathSegments(); - return pathSegments.size() == 2 && "tree".equals(pathSegments.get(0)); - } - - private static ContentResolver getContentResolver() - { - return DolphinApplication.getAppContext().getContentResolver(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.kt new file mode 100644 index 000000000000..b5e44568392e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.kt @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.content.ContentResolver +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document +import androidx.annotation.Keep +import androidx.core.net.toUri +import org.dolphinemu.dolphinemu.DolphinApplication +import java.io.FileNotFoundException + +/* + We use a lot of "catch (Exception e)" in this class. This is for two reasons: + + 1. We don't want any exceptions to escape to native code, as this leads to nasty crashes + that often don't have stack traces that make sense. + + 2. The sheer number of different exceptions, both documented and undocumented. These include: + - FileNotFoundException when a file doesn't exist + - FileNotFoundException when using an invalid open mode (according to the documentation) + - IllegalArgumentException when using an invalid open mode (in practice with FileProvider) + - IllegalArgumentException when providing a tree where a document was expected and vice versa + - SecurityException when trying to access something the user hasn't granted us permission to + - UnsupportedOperationException when a URI specifies a storage provider that doesn't exist + */ +object ContentHandler { + @JvmStatic + fun isContentUri(pathOrUri: String): Boolean { + return pathOrUri.startsWith("content://") + } + + @Keep + @JvmStatic + fun openFd(uri: String, mode: String): Int { + try { + return getContentResolver().openFileDescriptor(unmangle(uri), mode)!!.detachFd() + } catch (_: SecurityException) { + Log.error("Tried to open $uri without permission") + } catch (_: Exception) { + } + + return -1 + } + + @Keep + @JvmStatic + fun delete(uri: String): Boolean { + try { + return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri)) + } catch (_: FileNotFoundException) { + // Return true because we care about the file not being there, not the actual delete. + return true + } catch (_: SecurityException) { + Log.error("Tried to delete $uri without permission") + } catch (_: Exception) { + } + + return false + } + + @JvmStatic + fun exists(uri: String): Boolean { + try { + val documentUri = treeToDocument(unmangle(uri)) + val projection = arrayOf(Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE) + getContentResolver().query(documentUri, projection, null, null, null)?.use { cursor -> + return cursor.count > 0 + } + } catch (_: SecurityException) { + Log.error("Tried to check if $uri exists without permission") + } catch (_: Exception) { + } + + return false + } + + /** + * @return -1 if not found, -2 if directory, file size otherwise + */ + @Keep + @JvmStatic + fun getSizeAndIsDirectory(uri: String): Long { + try { + val documentUri = treeToDocument(unmangle(uri)) + val projection = arrayOf(Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE) + getContentResolver().query(documentUri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + return if (Document.MIME_TYPE_DIR == cursor.getString(0)) { + -2 + } else { + if (cursor.isNull(1)) 0 else cursor.getLong(1) + } + } + } + } catch (_: SecurityException) { + Log.error("Tried to get metadata for $uri without permission") + } catch (_: Exception) { + } + + return -1 + } + + @Keep + @JvmStatic + fun getDisplayName(uri: String): String? { + try { + return getDisplayName(unmangle(uri)) + } catch (_: Exception) { + } + + return null + } + + @JvmStatic + fun getDisplayName(uri: Uri): String? { + val projection = arrayOf(Document.COLUMN_DISPLAY_NAME) + val documentUri = treeToDocument(uri) + try { + getContentResolver().query(documentUri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getString(0) + } + } + } catch (_: SecurityException) { + Log.error("Tried to get display name of $uri without permission") + } catch (_: Exception) { + } + + return null + } + + @Keep + @JvmStatic + fun getChildNames(uri: String, recursive: Boolean): Array { + try { + return getChildNames(unmangle(uri), recursive) + } catch (_: Exception) { + } + + return emptyArray() + } + + @JvmStatic + fun getChildNames(uri: Uri, recursive: Boolean): Array { + val result = ArrayList() + fun addChildren(documentId: String) { + forEachChild(uri, documentId) { displayName, childDocumentId, isDirectory -> + if (recursive && isDirectory) { + addChildren(childDocumentId) + } else { + result.add(displayName) + } + } + } + + addChildren(DocumentsContract.getDocumentId(treeToDocument(uri))) + return result.toTypedArray() + } + + @Keep + @JvmStatic + fun doFileSearch( + directory: String, extensions: Array, recursive: Boolean + ): Array { + val result = ArrayList() + + try { + val uri = unmangle(directory) + val documentId = DocumentsContract.getDocumentId(treeToDocument(uri)) + val acceptAll = extensions.isEmpty() + val extensionCheck: (String) -> Boolean = { displayName -> + val extension = FileBrowserHelper.getExtension(displayName, true) + extension != null && extensions.any { it.equals(extension, ignoreCase = true) } + } + + fun addChildren(path: String, parentDocumentId: String) { + forEachChild(uri, parentDocumentId) { displayName, childDocumentId, isDirectory -> + val childPath = "$path/$displayName" + if (acceptAll || (!isDirectory && extensionCheck(displayName))) { + result.add(childPath) + } + if (recursive && isDirectory) { + addChildren(childPath, childDocumentId) + } + } + } + + addChildren(directory, documentId) + } catch (_: Exception) { + } + + return result.toTypedArray() + } + + private fun forEachChild( + uri: Uri, + documentId: String, + callback: (displayName: String, documentId: String, isDirectory: Boolean) -> Unit + ) { + try { + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId) + val projection = arrayOf( + Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, Document.COLUMN_DOCUMENT_ID + ) + getContentResolver().query(childrenUri, projection, null, null, null)?.use { cursor -> + while (cursor.moveToNext()) { + callback( + cursor.getString(0), + cursor.getString(2), + Document.MIME_TYPE_DIR == cursor.getString(1) + ) + } + } + } catch (_: SecurityException) { + Log.error("Tried to get children of $uri without permission") + } catch (_: Exception) { + } + } + + @Throws(FileNotFoundException::class, SecurityException::class) + private fun getChild(parentUri: Uri, childName: String): Uri { + val parentId = DocumentsContract.getDocumentId(treeToDocument(parentUri)) + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(parentUri, parentId) + + val projection = arrayOf(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_DOCUMENT_ID) + val selection = Document.COLUMN_DISPLAY_NAME + "=?" + val selectionArgs = arrayOf(childName) + try { + getContentResolver().query(childrenUri, projection, selection, selectionArgs, null) + ?.use { cursor -> + while (cursor.moveToNext()) { + // FileProvider seemingly doesn't support selections, so we have to manually filter here + if (childName == cursor.getString(0)) { + return DocumentsContract.buildDocumentUriUsingTree( + parentUri, cursor.getString(1) + ) + } + } + } + } catch (_: SecurityException) { + Log.error("Tried to get child $childName of $parentUri without permission") + } catch (_: Exception) { + } + + throw FileNotFoundException("$parentUri/$childName") + } + + /** + * Since our C++ code was written under the assumption that it would be running under a filesystem + * which supports normal paths, it appends a slash followed by a file name when it wants to access + * a file in a directory. This function translates that into the type of URI that SAF requires. + * + * In order to detect whether a URI is mangled or not, we make the assumption that an + * unmangled URI contains at least one % and does not contain any slashes after the last %. + * This seems to hold for all common storage providers, but it is theoretically possible for a + * storage provider to use URIs without any % characters. + */ + @Throws(FileNotFoundException::class, SecurityException::class) + @JvmStatic + fun unmangle(uri: String): Uri { + val lastComponentEnd = getLastComponentEnd(uri) + val lastComponentStart = getLastComponentStart(uri, lastComponentEnd) + + return if (lastComponentStart == 0) { + uri.substring(0, lastComponentEnd).toUri() + } else { + val parentUri = unmangle(uri.substring(0, lastComponentStart)) + val childName = uri.substring(lastComponentStart, lastComponentEnd) + getChild(parentUri, childName) + } + } + + /** + * Returns the last character which is not a slash. + */ + private fun getLastComponentEnd(uri: String): Int { + var i = uri.length + while (i > 0 && uri[i - 1] == '/') { + i-- + } + return i + } + + /** + * Scans backwards starting from lastComponentEnd and returns the index after the first slash + * it finds, but only if there is a % before that slash and there is no % after it. + */ + private fun getLastComponentStart(uri: String, lastComponentEnd: Int): Int { + var i = lastComponentEnd + while (i > 0 && uri[i - 1] != '/') { + i-- + if (uri[i] == '%') { + return 0 + } + } + + var j = i + while (j > 0) { + j-- + if (uri[j] == '%') { + return i + } + } + + return 0 + } + + private fun treeToDocument(uri: Uri): Uri { + return if (isTreeUri(uri)) { + val documentId = DocumentsContract.getTreeDocumentId(uri) + DocumentsContract.buildDocumentUriUsingTree(uri, documentId) + } else { + uri + } + } + + /** + * This is like DocumentsContract.isTreeUri, except it doesn't return true for URIs like + * content://com.example/tree/12/document/24/. We want to treat those as documents, not trees. + */ + private fun isTreeUri(uri: Uri): Boolean { + val pathSegments = uri.pathSegments + return pathSegments.size == 2 && pathSegments[0] == "tree" + } + + private fun getContentResolver(): ContentResolver { + return DolphinApplication.getAppContext().contentResolver + } +} From 8873338b8d59e7b675d31978e42b541fd4431d0a Mon Sep 17 00:00:00 2001 From: idkwhere1sthisname Date: Sat, 11 Jul 2026 19:10:20 +0200 Subject: [PATCH 115/270] allow Wii System Transfer and WagonCompat to call ES_SetUID --- Source/Core/Core/IOS/ES/ES.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/IOS/ES/ES.cpp b/Source/Core/Core/IOS/ES/ES.cpp index 962394872ed4..5366444e8876 100644 --- a/Source/Core/Core/IOS/ES/ES.cpp +++ b/Source/Core/Core/IOS/ES/ES.cpp @@ -283,7 +283,11 @@ static ReturnCode CheckIsAllowedToSetUID(EmulationKernel& kernel, ES::UIDSys* ui { const bool is_wiiu_transfer_tool = active_tmd.IsValid() && (active_tmd.GetTitleId() | 0xFF) == 0x00010001'484353ff; - if (is_wiiu_transfer_tool) + const bool is_wii_system_transfer = + active_tmd.IsValid() && (active_tmd.GetTitleId() | 0xFF) == 0x00010001'484354ff; + const bool is_wagoncompat_transfer = + active_tmd.IsValid() && (active_tmd.GetTitleId() | 0xFF) == 0x00010008'48435aff; + if (is_wiiu_transfer_tool || is_wii_system_transfer || is_wagoncompat_transfer) return IPC_SUCCESS; } From 37dd6c287eac591b4d97cbfd90d16c2093b68020 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Thu, 9 Jul 2026 21:11:43 -0700 Subject: [PATCH 116/270] ResourcePackManager: Fix crash when removing pack Avoid accessing destroyed `QTableWidgetItem` by retrieving the desired path from the item before it's destroyed. In `ResourcePackManager::Remove` pointers to the selected items in `m_table_widget` were saved in the local variable `items` before calling `Uninstall`. `Uninstall` called `RepopulateTable` which called `m_table_widget->clear()`, destroying the table's descendants. Upon returning to `Remove` `items` then pointed to some of those destroyed descendants, and passing `items[0]` to `GetResourcePackIndex` resulted in a call to `item->row()` which was a use-after-free. Fixes https://bugs.dolphin-emu.org/issues/14095. --- Source/Core/DolphinQt/ResourcePackManager.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Core/DolphinQt/ResourcePackManager.cpp b/Source/Core/DolphinQt/ResourcePackManager.cpp index 90739da45f15..221a752f57b3 100644 --- a/Source/Core/DolphinQt/ResourcePackManager.cpp +++ b/Source/Core/DolphinQt/ResourcePackManager.cpp @@ -3,6 +3,8 @@ #include "DolphinQt/ResourcePackManager.h" +#include + #include #include #include @@ -243,8 +245,10 @@ void ResourcePackManager::Remove() if (box.exec() != QMessageBox::Yes) return; + const std::string selected_pack_path = + ResourcePack::GetPacks()[GetResourcePackIndex(items[0])].GetPath(); Uninstall(); - File::Delete(ResourcePack::GetPacks()[GetResourcePackIndex(items[0])].GetPath()); + File::Delete(selected_pack_path); RepopulateTable(); } From a18fd2d21b82f33c5ec603d0a76e136db0e30a4c Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Fri, 10 Jul 2026 19:51:06 -0700 Subject: [PATCH 117/270] ResourcePackManager: Erase Removed pack from list and table Erase a removed pack from ResourcePack::::packs instead of only deleting the pack file from the disk. This causes the repopulated table to actually display that the pack has been removed (which previously only happened after restarting Dolphin or hitting `Refresh`), and also removes the pack from the `Packs.ini` file. --- Source/Core/DolphinQt/ResourcePackManager.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Core/DolphinQt/ResourcePackManager.cpp b/Source/Core/DolphinQt/ResourcePackManager.cpp index 221a752f57b3..c342b29bae65 100644 --- a/Source/Core/DolphinQt/ResourcePackManager.cpp +++ b/Source/Core/DolphinQt/ResourcePackManager.cpp @@ -17,6 +17,7 @@ #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/QtUtils/NonDefaultQPushButton.h" #include "UICommon/ResourcePack/Manager.h" +#include "UICommon/ResourcePack/ResourcePack.h" ResourcePackManager::ResourcePackManager(QWidget* widget) : QDialog(widget) { @@ -245,9 +246,11 @@ void ResourcePackManager::Remove() if (box.exec() != QMessageBox::Yes) return; - const std::string selected_pack_path = - ResourcePack::GetPacks()[GetResourcePackIndex(items[0])].GetPath(); + ResourcePack::ResourcePack& selected_pack = + ResourcePack::GetPacks()[GetResourcePackIndex(items[0])]; + const std::string selected_pack_path = selected_pack.GetPath(); Uninstall(); + ResourcePack::Remove(selected_pack); File::Delete(selected_pack_path); RepopulateTable(); } From 24ad5628717df1184fcbac027cd27618891afc40 Mon Sep 17 00:00:00 2001 From: cristian64 Date: Fri, 27 Mar 2026 11:30:52 +0000 Subject: [PATCH 118/270] Core: Add `Volume::GetSimulatedMemorySize()`. The data structure of the `bi2.bin` file in which the simulated memory size is set: > **13.2 Disk header Information** > > this is loaded to the Address in `0x800000f4` when a disc is initialized by the IPL > > | offset | end | size | Description | > | :------: | --- | :--: | --------------------- | > | `0x0000` | | 4 | Debug-monitor Size | > | `0x0004` | | 4 | Simulated Memory Size | > | `0x0008` | | 4 | Argument offset | > | `0x000c` | | 4 | Debug flag | > | `0x0010` | | 4 | Track Location | > | `0x0014` | | 4 | Track size | > | `0x0018` | | 4 | Countrycode | > | `0x001c` | | 4 | ? | See https://hitmen.c02.at/files/yagcd/yagcd/chap13.html#sec13.2. Most GameCube games set the value to `24 MiB`, which matches the physical memory size. Most Wii games set the value to `0 MiB` (unset). The debug build of _Mario Kart: Double Dash!!_ is one game that sets the value to `48 MiB`. --- Source/Core/DiscIO/Volume.h | 1 + Source/Core/DiscIO/VolumeDisc.cpp | 8 ++++++++ Source/Core/DiscIO/VolumeDisc.h | 1 + Source/Core/DiscIO/VolumeWad.h | 1 + 4 files changed, 11 insertions(+) diff --git a/Source/Core/DiscIO/Volume.h b/Source/Core/DiscIO/Volume.h index 7dfabb3e97fd..61df7e032fa7 100644 --- a/Source/Core/DiscIO/Volume.h +++ b/Source/Core/DiscIO/Volume.h @@ -126,6 +126,7 @@ class Volume } virtual Region GetRegion() const = 0; virtual Country GetCountry(const Partition& partition = PARTITION_NONE) const = 0; + virtual u32 GetSimulatedMemorySize() const = 0; virtual BlobType GetBlobType() const = 0; // Size of virtual disc (may be inaccurate depending on the blob type) virtual u64 GetDataSize() const = 0; diff --git a/Source/Core/DiscIO/VolumeDisc.cpp b/Source/Core/DiscIO/VolumeDisc.cpp index e842540376d4..4dd16c4cef17 100644 --- a/Source/Core/DiscIO/VolumeDisc.cpp +++ b/Source/Core/DiscIO/VolumeDisc.cpp @@ -179,6 +179,14 @@ std::optional VolumeDisc::GetDiscNumber(const Partition& partition) const return ReadSwapped(6, partition); } +u32 VolumeDisc::GetSimulatedMemorySize() const +{ + // Simulated memory size is a 32-bit unsigned integer in the `bi2.bin` file at offset `0x04`. + // Apploader reads this and "simulates" a smaller memory size. Originally useful for simulating + // 24 MiB of memory on devkits with 48 MiB. + return ReadSwapped(BI2_ADDRESS + 0x04, GetGamePartition()).value_or(0); +} + bool VolumeDisc::IsNKit() const { constexpr u32 NKIT_MAGIC = 0x4E4B4954; // "NKIT" diff --git a/Source/Core/DiscIO/VolumeDisc.h b/Source/Core/DiscIO/VolumeDisc.h index f630522e6811..1b3774a87d6b 100644 --- a/Source/Core/DiscIO/VolumeDisc.h +++ b/Source/Core/DiscIO/VolumeDisc.h @@ -47,6 +47,7 @@ class VolumeDisc : public Volume std::string GetInternalName(const Partition& partition = PARTITION_NONE) const override; std::string GetApploaderDate(const Partition& partition) const override; std::optional GetDiscNumber(const Partition& partition = PARTITION_NONE) const override; + u32 GetSimulatedMemorySize() const override; bool IsNKit() const override; protected: diff --git a/Source/Core/DiscIO/VolumeWad.h b/Source/Core/DiscIO/VolumeWad.h index 2c383c02543e..88a0847f80f6 100644 --- a/Source/Core/DiscIO/VolumeWad.h +++ b/Source/Core/DiscIO/VolumeWad.h @@ -61,6 +61,7 @@ class VolumeWAD final : public Volume bool IsNKit() const override; Region GetRegion() const override; Country GetCountry(const Partition& partition = PARTITION_NONE) const override; + u32 GetSimulatedMemorySize() const override { return 0; } BlobType GetBlobType() const override; u64 GetDataSize() const override; From ebcbb79f70a25fa75cbd30da39a65eac8dd9eddd Mon Sep 17 00:00:00 2001 From: cristian64 Date: Fri, 27 Mar 2026 11:48:18 +0000 Subject: [PATCH 119/270] Core: Add `SConfig::GetSimulatedMemorySize()`. This makes the simulated memory size of the running game accessible via the `SConfig` singleton. --- Source/Core/Core/ConfigManager.cpp | 25 ++++++++++++++++++------- Source/Core/Core/ConfigManager.h | 6 +++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 8404cfff07d7..460ca2e04885 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -154,27 +154,35 @@ u16 SConfig::GetRevision() const return m_revision; } +u32 SConfig::GetSimulatedMemorySize() const +{ + std::lock_guard lock(m_metadata_lock); + return m_simulated_memory_size; +} + void SConfig::ResetRunningGameMetadata() { std::lock_guard lock(m_metadata_lock); - SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown); + SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown, 0); } void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume, const DiscIO::Partition& partition) { std::lock_guard lock(m_metadata_lock); + if (partition == volume.GetGamePartition()) { SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(), volume.GetTitleID().value_or(0), volume.GetRevision().value_or(0), - volume.GetRegion()); + volume.GetRegion(), volume.GetSimulatedMemorySize()); } else { SetRunningGameMetadata(volume.GetGameID(partition), volume.GetGameTDBID(partition), volume.GetTitleID(partition).value_or(0), - volume.GetRevision(partition).value_or(0), volume.GetRegion()); + volume.GetRevision(partition).value_or(0), volume.GetRegion(), + volume.GetSimulatedMemorySize()); } } @@ -192,26 +200,29 @@ void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Plat { // If not launching a disc game, just read everything from the TMD. SetRunningGameMetadata(tmd.GetGameID(), tmd.GetGameTDBID(), tmd_title_id, tmd.GetTitleVersion(), - tmd.GetRegion()); + tmd.GetRegion(), 0); } } void SConfig::SetRunningGameMetadata(const std::string& game_id) { std::lock_guard lock(m_metadata_lock); - SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown); + SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown, 0); } void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id, - u64 title_id, u16 revision, DiscIO::Region region) + u64 title_id, u16 revision, DiscIO::Region region, + u32 simulated_memory_size) { std::lock_guard lock(m_metadata_lock); const bool was_changed = m_game_id != game_id || m_gametdb_id != gametdb_id || - m_title_id != title_id || m_revision != revision; + m_title_id != title_id || m_revision != revision || + m_simulated_memory_size != simulated_memory_size; m_game_id = game_id; m_gametdb_id = gametdb_id; m_title_id = title_id; m_revision = revision; + m_simulated_memory_size = simulated_memory_size; if (game_id.length() == 6) { diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index 39dbc6d39ae6..145b00241801 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -65,6 +65,7 @@ struct SConfig const std::string GetTitleDescription() const; u64 GetTitleID() const; u16 GetRevision() const; + u32 GetSimulatedMemorySize() const; void ResetRunningGameMetadata(); void SetRunningGameMetadata(const DiscIO::Volume& volume, const DiscIO::Partition& partition); void SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform); @@ -119,7 +120,8 @@ struct SConfig static void ReloadTextures(Core::System& system); void SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id, - u64 title_id, u16 revision, DiscIO::Region region); + u64 title_id, u16 revision, DiscIO::Region region, + u32 simulated_memory_size); static SConfig* m_Instance; mutable std::recursive_mutex m_metadata_lock; @@ -130,4 +132,6 @@ struct SConfig std::string m_title_description; u64 m_title_id; u16 m_revision; + // Memory size requested by game's header. + u32 m_simulated_memory_size; }; From 03b1f2148b080adc9ed15ecc40e3948404388b61 Mon Sep 17 00:00:00 2001 From: cristian64 Date: Mon, 29 Jun 2026 22:29:37 +0100 Subject: [PATCH 120/270] DolphinAnalytics: Add game quirk for game simulated memory size. This is the 32-bit unsigned integer that is seen at offset `0x04` in the `bi2.bin` file. The intent is to study what games define values that departure from the norm. Common values are 0 MiB (Wii games) and 24 MiB (GameCube games), with some debug and demo builds featuring 48 MiB (e.g. the debug build of _Mario Kart: Double Dash!!_). --- Source/Core/Core/DolphinAnalytics.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Core/Core/DolphinAnalytics.cpp b/Source/Core/Core/DolphinAnalytics.cpp index 1fff6bc5c508..8ae3bf6bcfbe 100644 --- a/Source/Core/Core/DolphinAnalytics.cpp +++ b/Source/Core/Core/DolphinAnalytics.cpp @@ -354,6 +354,9 @@ void DolphinAnalytics::MakePerGameBuilder() // Unique id bound to the gameid. builder.AddData("id", MakeUniqueId(SConfig::GetInstance().GetGameID())); + // Other game-specific fields. + builder.AddData("simulated-memory-size", SConfig::GetInstance().GetSimulatedMemorySize()); + // Configuration. builder.AddData("cfg-dsp-hle", Config::Get(Config::MAIN_DSP_HLE)); builder.AddData("cfg-dsp-jit", Config::Get(Config::MAIN_DSP_JIT)); From 0cab1731d61ecb87c7bc388dbd176a4150afbbe3 Mon Sep 17 00:00:00 2001 From: cristian64 Date: Fri, 3 Apr 2026 10:58:54 +0100 Subject: [PATCH 121/270] Core: Adjust emulated memory size automatically. Dolphin now uses the simulated memory size defined in `bi2.bin` to adjust the memory size in the emulated console automatically. If **Enable Emulated Memory Size Override** has been enabled by the user, the fixed memory size specified for **MEM1** and **MEM2** are still used as they were before. Most retail games do not define the simulated memory size (Wii or Triforce games), or define it to a value that matches the default 24 MiB (GameCube games), so, in the general case, there is no behavior change. One game that sets the simulated memory to a non-default value is the debug build of _Mario Kart: Double Dash!!_, where the value is set to 48 MiB. This enhancement is focused mainly to the modding community. Prior to these changes, modded games with extended memory requirements would fail to launch in Dolphin [with no indication of what the problem is] if the user failed to set the emulated memory override to the correct value. Now, modding tools can specify the simulated memory size in the `bi2.bin` file to produce extended games that _just work_ in Dolphin, without cumbersome instructions that can be overlooked by the user. --- Source/Core/Core/FifoPlayer/FifoDataFile.cpp | 3 ++- Source/Core/Core/HW/Memmap.cpp | 13 +++++++++++-- Source/Core/Core/HW/Memmap.h | 2 ++ Source/Core/Core/IOS/IOS.cpp | 5 ++++- Source/Core/Core/System.cpp | 1 + Source/Core/Core/System.h | 4 ++++ .../Core/PowerPC/PageTableHostMappingTest.cpp | 5 +++++ 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/FifoPlayer/FifoDataFile.cpp b/Source/Core/Core/FifoPlayer/FifoDataFile.cpp index f795d5f08a2b..0e1e44aef975 100644 --- a/Source/Core/Core/FifoPlayer/FifoDataFile.cpp +++ b/Source/Core/Core/FifoPlayer/FifoDataFile.cpp @@ -140,7 +140,8 @@ bool FifoDataFile::Save(const std::string& filename) header.fileId = FILE_ID; header.file_version = VERSION_NUMBER; // Maintain backwards compatibility so long as the RAM sizes aren't overridden. - if (Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE)) + if (Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE) || + SConfig::GetInstance().GetSimulatedMemorySize() > Memory::MEM1_SIZE_RETAIL) header.min_loader_version = MIN_LOADER_VERSION_FOR_RAM_OVERRIDE; else header.min_loader_version = MIN_LOADER_VERSION; diff --git a/Source/Core/Core/HW/Memmap.cpp b/Source/Core/Core/HW/Memmap.cpp index e6d8fd56a55b..054596e3d80b 100644 --- a/Source/Core/Core/HW/Memmap.cpp +++ b/Source/Core/Core/HW/Memmap.cpp @@ -25,6 +25,7 @@ #include "Common/MsgHandler.h" #include "Common/Swap.h" #include "Core/Config/MainSettings.h" +#include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/HW/AudioInterface.h" #include "Core/HW/DSP.h" @@ -87,10 +88,18 @@ void MemoryManager::InitMMIO(Core::System& system) void MemoryManager::Init() { - const auto get_mem1_size = [] { + const auto get_mem1_size = [this] { if (Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE)) return Config::Get(Config::MAIN_MEM1_SIZE); - return Memory::MEM1_SIZE_RETAIL; + // The simulated memory size in the game's header was originally used to ask the Apploader to + // simulate a smaller memory size than that in development systems. That is, it could make the + // 48 MiB devkit act like a 24 MiB retail console during testing, or any other config. + // + // Dolphin is reusing this as a signal to automatically adjust the emulated memory size. The + // upper bound is clamped to 64 MiB (maximum supported by Flipper's memory controller), + // reflecting Apploader behaviour of clamping the simulated size to physical memory size. It is + // never desired to emulate less than a retail console, so the lower bound is clapped to 24 MiB. + return std::clamp(m_system.GetSimulatedMemorySize(), MEM1_SIZE_RETAIL, MEM1_SIZE_MAX); }; const auto get_mem2_size = [] { if (Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE)) diff --git a/Source/Core/Core/HW/Memmap.h b/Source/Core/Core/HW/Memmap.h index baa910cc70d6..e9253272f6c0 100644 --- a/Source/Core/Core/HW/Memmap.h +++ b/Source/Core/Core/HW/Memmap.h @@ -34,8 +34,10 @@ constexpr u32 MEM1_BASE_ADDR = 0x80000000U; constexpr u32 MEM2_BASE_ADDR = 0x90000000U; constexpr u32 MEM1_SIZE_RETAIL = 0x01800000U; constexpr u32 MEM1_SIZE_GDEV = 0x04000000U; +constexpr u32 MEM1_SIZE_MAX = 0x04000000U; constexpr u32 MEM2_SIZE_RETAIL = 0x04000000U; constexpr u32 MEM2_SIZE_NDEV = 0x08000000U; +constexpr u32 MEM2_SIZE_MAX = 0x08000000U; struct PhysicalMemoryRegion { diff --git a/Source/Core/Core/IOS/IOS.cpp b/Source/Core/Core/IOS/IOS.cpp index d3986775b67c..49c0b2bbd3ca 100644 --- a/Source/Core/Core/IOS/IOS.cpp +++ b/Source/Core/Core/IOS/IOS.cpp @@ -221,8 +221,11 @@ static void ReleasePPCAncast(Core::System& system) void RAMOverrideForIOSMemoryValues(Memory::MemoryManager& memory, MemorySetupType setup_type) { // Don't touch anything if the feature isn't enabled. - if (!Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE)) + if (!Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE) || + SConfig::GetInstance().GetSimulatedMemorySize() <= Memory::MEM1_SIZE_RETAIL) + { return; + } // Some unstated constants that can be inferred. const u32 ipc_buffer_size = diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index 686d1a32641a..12da7dcaeab0 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -119,6 +119,7 @@ void System::Initialize() m_separate_cpu_and_gpu_threads = Config::Get(Config::MAIN_CPU_THREAD); m_mmu_enabled = Config::Get(Config::MAIN_MMU); m_pause_on_panic_enabled = Config::Get(Config::MAIN_PAUSE_ON_PANIC); + m_simulated_memory_size = SConfig::GetInstance().GetSimulatedMemorySize(); } SoundStream* System::GetSoundStream() const diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index a170c608ee27..85240034007b 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -5,6 +5,8 @@ #include +#include "Common/CommonTypes.h" + class GeometryShaderManager; class Interpreter; class JitInterface; @@ -147,6 +149,7 @@ class System bool IsTriforce() const { return m_is_triforce; } bool IsWii() const { return m_is_wii; } bool IsBranchWatchIgnoreApploader() { return m_branch_watch_ignore_apploader; } + u32 GetSimulatedMemorySize() const { return m_simulated_memory_size; } void SetIsMIOS(bool is_mios) { m_is_mios = is_mios; } void SetIsTriforce(bool is_triforce) { m_is_triforce = is_triforce; } @@ -216,5 +219,6 @@ class System bool m_is_triforce = false; bool m_is_wii = false; bool m_branch_watch_ignore_apploader = false; + u32 m_simulated_memory_size{0}; }; } // namespace Core diff --git a/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp b/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp index 2cfb4b8c8ff9..c2622cee3776 100644 --- a/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp +++ b/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp @@ -10,6 +10,7 @@ #include "Common/Align.h" #include "Common/CommonTypes.h" #include "Common/Swap.h" +#include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/MemTools.h" #include "Core/PowerPC/BreakPoints.h" @@ -131,6 +132,8 @@ class PageTableHostMappingTest : public ::testing::Test if (!EMM::IsExceptionHandlerSupported()) GTEST_SKIP() << "Skipping PageTableHostMappingTest because exception handler is unsupported."; + SConfig::Init(); + auto& system = Core::System::GetInstance(); auto& memory = system.GetMemory(); const u32 host_page_size = memory.GetHostPageSize(); @@ -177,6 +180,8 @@ class PageTableHostMappingTest : public ::testing::Test EMM::UninstallExceptionHandler(); Core::UndeclareAsCPUThread(); system.GetMemory().Shutdown(); + + SConfig::Shutdown(); } static void SetSR(size_t index, u32 vsid) From 594e5d2b56e441760888d56b2d7633520f7770f5 Mon Sep 17 00:00:00 2001 From: cristian64 Date: Sat, 30 May 2026 13:09:58 +0100 Subject: [PATCH 122/270] DolphinQt: Update tool tip for Enable Emulated Memory Size Override. --- Source/Core/DolphinQt/Settings/AdvancedPane.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp index a526e8c7cb94..a2fc9c54102a 100644 --- a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp +++ b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp @@ -276,10 +276,9 @@ void AdvancedPane::CreateLayout() }); m_ram_override_checkbox->SetDescription( - tr("Adjusts the amount of RAM in the emulated console.

" - "WARNING: Enabling this will completely break many games.
Only a small " - "number " - "of games can benefit from this." + tr("Sets the amount of RAM in the emulated console to the values provided.

" + "WARNING: Enabling this will completely break many games. By default, Dolphin " + "determines what value is required based on the game information." "

If unsure, leave this unchecked.")); auto* rtc_options = new QGroupBox(tr("Custom RTC Options")); From f9ef6c2151d48ef9b5dbe9ee1c2278e5d60b5ed9 Mon Sep 17 00:00:00 2001 From: cristian64 Date: Sat, 30 May 2026 13:12:24 +0100 Subject: [PATCH 123/270] Android: Update description for Enable Emulated Memory Size Override. --- Source/Android/app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 07c5e7ebc003..9d97169d951c 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -400,7 +400,7 @@ Adjusts the emulated CPU\'s clock rate if \"Override Emulated CPU Clock Speed\" is enabled. Memory Override Enable Emulated Memory Size Override - Adjusts the amount of RAM in the emulated console.\n\nWARNING: Enabling this will completely break many games. Only a small number of games can benefit from this. + Sets the amount of RAM in the emulated console to the values provided.\n\nWARNING: Enabling this will completely break many games. By default, Dolphin determines what value is required based on the game information. MEM1 Size MEM2 Size GPU Options From 05855a37f4d6b9e98e41e7c50983faa167ae03ac Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 12 Jul 2026 10:05:14 +0200 Subject: [PATCH 124/270] GCAdapter: Deregister callback when scan thread exits We were registering the hotplug callback when starting the thread, but deregistering it when shutting down GCAdapter as a whole. Because the thread can start and stop potentially many times before GCAdapter shuts down, this led to callbacks being registered when we already have callbacks. On Android, this was making Dolphin's Kotlin code for registering the callback throw an IllegalStateException. To fix this, deregister the callback when stopping the thread. --- Source/Core/InputCommon/GCAdapter.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Source/Core/InputCommon/GCAdapter.cpp b/Source/Core/InputCommon/GCAdapter.cpp index e11d95d68f94..dbffc4d81def 100644 --- a/Source/Core/InputCommon/GCAdapter.cpp +++ b/Source/Core/InputCommon/GCAdapter.cpp @@ -473,6 +473,17 @@ static void ScanThreadFunc() } #endif +#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION +#if LIBUSB_API_HAS_HOTPLUG + if (s_libusb_hotplug_enabled) + libusb_hotplug_deregister_callback(*s_libusb_context, s_hotplug_handle); +#endif +#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION + const jmethodID disable_hotplug_callback_func = + env->GetStaticMethodID(s_adapter_class, "disableHotplugCallback", "()V"); + env->CallStaticVoidMethod(s_adapter_class, disable_hotplug_callback_func); +#endif + NOTICE_LOG_FMT(CONTROLLERINTERFACE, "GC Adapter scanning thread stopped"); } @@ -761,17 +772,6 @@ static void AddGCAdapter(libusb_device* device) void Shutdown() { StopScanThread(); -#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION -#if LIBUSB_API_HAS_HOTPLUG - if (s_libusb_context && s_libusb_context->IsValid() && s_libusb_hotplug_enabled) - libusb_hotplug_deregister_callback(*s_libusb_context, s_hotplug_handle); -#endif -#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION - JNIEnv* const env = IDCache::GetEnvForThread(); - const jmethodID disable_hotplug_callback_func = - env->GetStaticMethodID(s_adapter_class, "disableHotplugCallback", "()V"); - env->CallStaticVoidMethod(s_adapter_class, disable_hotplug_callback_func); -#endif Reset(); #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION From 4b55c26cdbfbdc6434dbbbf78fbe89650262e5b5 Mon Sep 17 00:00:00 2001 From: Craig Carnell <1188869+cscd98@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:53:59 +0100 Subject: [PATCH 125/270] m3u: normalize line endings except win32 --- Source/Core/Core/Boot/Boot.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 2ddadfec720b..e6ce8accfd86 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -76,6 +76,11 @@ static std::vector ReadM3UFile(const std::string& m3u_path, // This is the UTF-8 representation of U+FEFF. constexpr std::string_view utf8_bom = "\xEF\xBB\xBF"; +#ifndef _WIN32 + if (line.ends_with('\r')) + line.pop_back(); +#endif + if (line.starts_with(utf8_bom)) { WARN_LOG_FMT(BOOT, "UTF-8 BOM in file: {}", m3u_path); From aeb3fec03aa31fbd14420c10653b27999feffc7d Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Sun, 12 Jul 2026 10:17:29 -0400 Subject: [PATCH 126/270] Android: Update dependencies and build tools Update Android dependencies and build tools to their latest available versions. Rename the GameCube save magic constant to avoid a collision with the NDK 30 ARM64 signal context header. --- Source/Android/app/build.gradle.kts | 12 +- Source/Android/benchmark/build.gradle.kts | 4 +- Source/Android/gradle.properties | 2 +- Source/Android/gradle/libs.versions.toml | 20 +- .../Android/gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- Source/Android/gradlew | 6 +- Source/Android/gradlew.bat | 175 ++++++++---------- Source/Android/settings.gradle.kts | 15 -- .../Core/Core/HW/GCMemcard/GCMemcardUtils.cpp | 6 +- 10 files changed, 107 insertions(+), 137 deletions(-) diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 11473af09f9c..64abfd003746 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -7,8 +7,8 @@ plugins { @Suppress("UnstableApiUsage") android { - compileSdk = 36 - ndkVersion = "29.0.14206865" + compileSdk = 37 + ndkVersion = "30.0.15729638" buildFeatures { compose = true @@ -38,7 +38,7 @@ android { defaultConfig { applicationId = "org.dolphinemu.dolphinemu" minSdk = 24 - targetSdk = 36 + targetSdk = 37 versionCode = getBuildVersionCode() @@ -118,12 +118,6 @@ android { } } -kotlin { - compilerOptions { - freeCompilerArgs.add("-Xannotation-default-target=param-property") - } -} - dependencies { baselineProfile(project(":benchmark")) coreLibraryDesugaring(libs.desugar.jdk.libs) diff --git a/Source/Android/benchmark/build.gradle.kts b/Source/Android/benchmark/build.gradle.kts index 21efb6a0d635..2244eb8399b8 100644 --- a/Source/Android/benchmark/build.gradle.kts +++ b/Source/Android/benchmark/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "org.dolphinemu.baselineprofile" - compileSdk = 36 + compileSdk = 37 compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -16,7 +16,7 @@ android { defaultConfig { minSdk = 28 - targetSdk = 36 + targetSdk = 37 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/Source/Android/gradle.properties b/Source/Android/gradle.properties index 42e35cc9115c..7449fbed0b03 100644 --- a/Source/Android/gradle.properties +++ b/Source/Android/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 android.enableJetifier=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK diff --git a/Source/Android/gradle/libs.versions.toml b/Source/Android/gradle/libs.versions.toml index f9c4992ccdfa..a4311b8e5a3a 100644 --- a/Source/Android/gradle/libs.versions.toml +++ b/Source/Android/gradle/libs.versions.toml @@ -1,31 +1,31 @@ [versions] -agp = "9.1.0" +agp = "9.2.1" appcompat = "1.7.1" -benchmarkMacroJunit4 = "1.5.0-alpha04" +benchmarkMacroJunit4 = "1.5.0-alpha07" cardview = "1.0.0" coil = "2.7.0" -compose-bom = "2026.04.01" +compose-bom = "2026.06.01" constraintlayout = "2.2.1" -coreKtx = "1.18.0" +coreKtx = "1.19.0" coreSplashscreen = "1.2.0" desugar_jdk_libs = "2.1.5" espressoCore = "3.7.0" filepicker = "4.2.1" fragmentKtx = "1.8.9" junit = "1.3.0" -kotlin = "2.3.20" -kotlinxCoroutinesAndroid = "1.10.2" -kotlinxSerializationJson = "1.10.0" +kotlin = "2.4.0" +kotlinxCoroutinesAndroid = "1.11.0" +kotlinxSerializationJson = "1.11.0" leanback = "1.2.0" -lifecycle = "2.10.0" -material = "1.13.0" +lifecycle = "2.11.0" +material = "1.14.0" preferenceKtx = "1.2.1" profileinstaller = "1.4.1" recyclerview = "1.4.0" slidingpanelayout = "1.2.0" swiperefreshlayout = "1.2.0" tvprovider = "1.1.0" -uiautomator = "2.3.0" +uiautomator = "2.4.0" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose" } diff --git a/Source/Android/gradle/wrapper/gradle-wrapper.jar b/Source/Android/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/Source/Android/settings.gradle.kts b/Source/Android/settings.gradle.kts index 1c44ac661311..1a68f74a481a 100644 --- a/Source/Android/settings.gradle.kts +++ b/Source/Android/settings.gradle.kts @@ -1,19 +1,4 @@ pluginManagement { - buildscript { - repositories { - mavenCentral() - maven { - url = uri("https://storage.googleapis.com/r8-releases/raw") - } - } - dependencies { - // Temporary override for AGP-bundled R8 crash in release minification. - // https://issuetracker.google.com/issues/495458806 - // TODO: Re-test without this override when upgrading AGP and remove if fixed upstream. - classpath("com.android.tools:r8:9.1.34") - } - } - repositories { gradlePluginPortal() google() diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp index c7e0aa06a8b8..7d097d61e1eb 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp @@ -21,7 +21,7 @@ constexpr std::array SAV_MAGIC = {0x44, 0x41, 0x54, 0x45, 0x4C, 0x47, 0x43, 0x5F, 0x53, 0x41, 0x56, 0x45}; // "DATELGC_SAVE" constexpr u32 SAV_HEADER_SIZE = 0xC0; constexpr u32 SAV_DENTRY_OFFSET = 0x80; -constexpr std::array GCS_MAGIC = {0x47, 0x43, 0x53, 0x41, 0x56, 0x45}; // "GCSAVE" +constexpr std::array GCS_FILE_MAGIC = {0x47, 0x43, 0x53, 0x41, 0x56, 0x45}; // "GCSAVE" constexpr u32 GCS_HEADER_SIZE = 0x150; constexpr u32 GCS_DENTRY_OFFSET = 0x110; @@ -153,7 +153,7 @@ static std::variant ReadSavefileInternalGCS(Fil if (!file.ReadBytes(gcs_header.data(), gcs_header.size())) return ReadSavefileErrorCode::IOError; - if (std::memcmp(gcs_header.data(), GCS_MAGIC.data(), GCS_MAGIC.size()) != 0) + if (std::memcmp(gcs_header.data(), GCS_FILE_MAGIC.data(), GCS_FILE_MAGIC.size()) != 0) return ReadSavefileErrorCode::DataCorrupted; Savefile savefile; @@ -245,7 +245,7 @@ static bool WriteSavefileInternalGCS(File::IOFile& file, const Savefile& savefil { std::array header; std::memset(header.data(), 0, header.size()); - std::memcpy(header.data(), GCS_MAGIC.data(), GCS_MAGIC.size()); + std::memcpy(header.data(), GCS_FILE_MAGIC.data(), GCS_FILE_MAGIC.size()); DEntry gcs_entry = savefile.dir_entry; gcs_entry.m_block_count = 1; // always stored as 1 in GCS files From 55761d9e6deca298a6f97941f03ffa56dc36bec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Mon, 13 Jul 2026 02:26:16 +0200 Subject: [PATCH 127/270] PerformanceMetrics: Fix compiling using clang&libstdc++ When using clang and libstdc++, compiling fails with these errors: ``` FAILED: [code=1] Source/Core/VideoCommon/CMakeFiles/videocommon.dir/PerformanceMetrics.cpp.o /usr/sbin/ccache /usr/sbin/clang++ -DAUTOUPDATE=1 -DDATA_DIR=\"/usr/local/share/dolphin-emu/\" -DHAS_OPENGL -DHAS_VULKAN -DHAVE_CPPIPC -DHAVE_LIBSYSTEMD -DIMGUI_USER_CONFIG=\"imgui_user_config.h\" -DLZMA_API_STATIC -DOFF -DPUGIXML_NO_EXCEPTIONS -DRC_CLIENT_SUPPORTS_HASH -DSFML_STATIC -DSPNG_STATIC -DTINYGLTF_NOEXCEPTION -DTINYGLTF_NO_EXTERNAL_IMAGE -DTINYGLTF_USE_CPP14 -DUSE_ANALYTICS=1 -DUSE_MEMORYWATCHER=1 -DUSE_PIPES=1 -DUSE_RETRO_ACHIEVEMENTS -DZSTD_MULTITHREAD -D_ARCH_64=1 -D_DEFAULT_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_M_X86_64=1 -D__LIBUSB__ -D__STDC_CONSTANT_MACROS -D__STDC_LIMIT_MACROS -I/dolphin-local/Source/Core -I/dolphin-local/Externals/picojson -I/dolphin-local/build/release/x64/Source/Core -I/dolphin-local/Externals/enet/enet/include -I/dolphin-local/Externals/fmt/fmt/include -I/dolphin-local/Externals/mbedtls/include -I/dolphin-local/Externals/minizip-ng/minizip-ng -I/dolphin-local/Externals/liblzma/api -I/dolphin-local/Externals/zstd/zstd/build/cmake/../../lib -I/dolphin-local/Externals/libiconv/include -I/dolphin-local/build/release/x64/Externals/libiconv/lib -I/dolphin-local/Externals/libiconv/libcharset/include -I/dolphin-local/build/release/x64/Externals/libiconv/libcharset -I/dolphin-local/Externals/SFML/SFML/include -I/dolphin-local/Externals/bzip2/bzip2 -I/dolphin-local/Externals/expr/include -I/dolphin-local/Externals/hidapi/hidapi-src/hidapi -I/dolphin-local/Externals/pugixml/pugixml/src -I/dolphin-local/Externals/glslang/glslang/glslang/.. -I/dolphin-local/build/release/x64/include -I/dolphin-local/Externals/rcheevos -I/dolphin-local/Externals/libspng/libspng/spng -I/dolphin-local/Externals/xxhash/xxHash -I/dolphin-local/Externals/imgui -I/dolphin-local/Externals/imgui/imgui -I/dolphin-local/Externals/implot/implot -I/dolphin-local/Externals/glslang/glslang/SPIRV/.. -I/dolphin-local/Externals/tinygltf -isystem /dolphin-local/Externals/libusb/libusb/libusb -isystem /dolphin-local/Externals/glslang/glslang -isystem /dolphin-local/Externals/glslang/glslang/Public -isystem /dolphin-local/Externals/glslang -Qunused-arguments -fcolor-diagnostics -O3 -DNDEBUG -std=c++23 -msse2 -fdiagnostics-color -fno-strict-aliasing -fno-exceptions -fvisibility-inlines-hidden -fvisibility=hidden -fomit-frame-pointer -Wall -Wtype-limits -Wsign-compare -Wignored-qualifiers -Wuninitialized -Wshadow -Wshadow-field-in-constructor -Winit-self -Wmissing-declarations-Wmissing-variable-declarations -Werror=format -Wno-trigraphs -MD -MT Source/Core/VideoCommon/CMakeFiles/videocommon.dir/PerformanceMetrics.cpp.o -MF Source/Core/VideoCommon/CMakeFiles/videocommon.dir/PerformanceMetrics.cpp.o.d -o Source/Core/VideoCommon/CMakeFiles/videocommon.dir/PerformanceMetrics.cpp.o -c /dolphin-local/Source/Core/VideoCommon/PerformanceMetrics.cpp In file included from /dolphin-local/Source/Core/VideoCommon/PerformanceMetrics.cpp:4: /dolphin-local/Source/Core/VideoCommon/PerformanceMetrics.h:69:51: error: no matching constructor for initialization of 'std::atomic' 69 | std::atomic m_frame_buffer_size{}; | ^~ /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/atomic:237:17: note: candidate constructor not viable: constraints not satisfied 237 | constexpr atomic() noexcept(is_nothrow_default_constructible_v<_Tp>) | ^ /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/atomic:238:11: note: because 'is_default_constructible_v' evaluated to false 238 | requires is_default_constructible_v<_Tp> | ^ /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/atomic:246:7: note: candidate constructor not viable: requires 1 argument, but 0 were provided 246 | atomic(const atomic&) = delete; | ^ ~~~~~~~~~~~~~ /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/atomic:252:17: note: candidate constructor not viable: requires single argument '__i', but no arguments were provided 252 | constexpr atomic(_Tp __i) noexcept : _M_i(__i) | ^ ~~~~~~~ 1 error generated. ``` This was tested and occurs on both Arch and Alpine Linux. --- Source/Core/VideoCommon/PerformanceMetrics.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Core/VideoCommon/PerformanceMetrics.h b/Source/Core/VideoCommon/PerformanceMetrics.h index b24c6f665988..698dd4412a7d 100644 --- a/Source/Core/VideoCommon/PerformanceMetrics.h +++ b/Source/Core/VideoCommon/PerformanceMetrics.h @@ -51,8 +51,10 @@ class PerformanceMetrics private: struct FrameBufferSize { - u32 width = 0; - u32 height = 0; + FrameBufferSize() : width(0), height(0) {} + FrameBufferSize(u32 w, u32 h) : width(w), height(h) {} + u32 width; + u32 height; }; PerformanceTracker m_fps_counter{"render_times.txt"}; From 009a87de59951bc4a4484d379f77cf9ed64e04d2 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 11 May 2026 07:33:16 -0700 Subject: [PATCH 128/270] Scaffold / Appbar composable Custom composable that switched between compact and medium size app bar depending on a combination of orientation and screen size. (Small app bar when horizontal on small devices, medium app bar otherwise. Medium app bar supports collapse on scroll. --- .../features/netplay/ui/NetplayScreen.kt | 42 +++++++++-------- .../features/netplay/ui/NetplaySetupScreen.kt | 25 ++++------ .../dolphinemu/ui/theme/DolphinTheme.kt | 46 +++++++++++++++++++ 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index d3a05d3fcfa6..789a3cef1764 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.Intent import android.content.res.Configuration +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -45,6 +46,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExtendedFloatingActionButton @@ -54,11 +56,8 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -105,6 +104,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox @@ -142,19 +142,19 @@ fun NetplayScreen( gameDigestProgress: GameDigestProgress?, joinAddresses: Map, ) { - Scaffold( - topBar = { - MediumTopAppBar( - title = { Text(stringResource(R.string.netplay_title)) }, - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) + val scrollState = rememberScrollState() + + DolphinScaffold( + title = { + Text(stringResource(R.string.netplay_title)) + }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } }, floatingActionButton = { if (isHosting) { @@ -204,6 +204,7 @@ fun NetplayScreen( joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, + scrollState = scrollState, contentPadding = innerPadding, modifier = modifier ) @@ -230,6 +231,7 @@ fun NetplayScreen( joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, + scrollState = scrollState, contentPadding = innerPadding, modifier = modifier ) @@ -348,12 +350,13 @@ private fun PortraitContent( joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, + scrollState: ScrollState, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) .padding(contentPadding) ) { Chat( @@ -420,6 +423,7 @@ private fun LandscapeContent( joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, + scrollState: ScrollState, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { @@ -847,7 +851,7 @@ private fun JoinInfoDropdown( label = { Text(stringResource(R.string.netplay_host_address_label)) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth() ) @@ -1005,7 +1009,7 @@ private fun NetworkModeDropdown( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), ) ExposedDropdownMenu( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt index f7f660638d10..ae222e8722b4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -30,11 +30,9 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text @@ -57,6 +55,7 @@ import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType +import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer @@ -88,19 +87,15 @@ fun NetplaySetupScreen( onHostClicked: () -> Unit, onConnectClicked: () -> Unit, ) { - Scaffold( - topBar = { - MediumTopAppBar( - title = { Text(stringResource(R.string.netplay_setup_title)) }, - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - ) + DolphinScaffold( + title = { Text(stringResource(R.string.netplay_setup_title)) }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } }, floatingActionButton = { ExtendedFloatingActionButton( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index c142a44d73db..4aa7e2055a80 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -3,6 +3,7 @@ package org.dolphinemu.dolphinemu.ui.theme import android.content.Context +import android.content.res.Configuration import androidx.annotation.AttrRes import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.ScrollState @@ -21,12 +22,16 @@ import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.SheetValue.Hidden import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @@ -41,6 +46,8 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle @@ -147,6 +154,45 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme { } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DolphinScaffold( + title: @Composable () -> Unit, + navigationIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, + floatingActionButton: @Composable () -> Unit = {}, + content: @Composable (PaddingValues) -> Unit, +) { + val isCompactLandscape = with(LocalConfiguration.current) { + orientation == Configuration.ORIENTATION_LANDSCAPE && smallestScreenWidthDp < 600 + } + + val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + Scaffold( + modifier = modifier.then( + if (isCompactLandscape) Modifier + else Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) + ), + topBar = { + if (isCompactLandscape) { + TopAppBar( + title = title, + navigationIcon = navigationIcon, + ) + } else { + MediumTopAppBar( + title = title, + navigationIcon = navigationIcon, + scrollBehavior = topAppBarScrollBehavior, + ) + } + }, + floatingActionButton = floatingActionButton, + content = content, + ) +} + @Composable fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp)) From cdb93d0c99444b7d2c2a00b4264aec5aee4676ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Tue, 14 Jul 2026 14:51:38 +0200 Subject: [PATCH 129/270] Contributing.md: Update C++20 mentions to C++23 --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 49496cf222b7..1441a22245aa 100644 --- a/Contributing.md +++ b/Contributing.md @@ -195,7 +195,7 @@ Summary: - [Classes and Structs](#cpp-code-classes-and-structs) ## General -- The codebase currently uses C++20, though not all compilers support all C++20 features. +- The codebase currently uses C++23, though compiler support for newer language features varies. - See CMakeLists.txt "Enforce minimum compiler versions" for the currently supported compilers. - Use the [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) type over the macro `NULL`. - If a [range-based for loop](https://en.cppreference.com/w/cpp/language/range-for) can be used instead of container iterators, use it. From baf154de0e1f518cf48e85ef5c1a2e5196d93d74 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 28 Jun 2026 11:05:52 +0200 Subject: [PATCH 130/270] DiscIO: Add DTK alignment comment in DirectoryBlob This requirement came up in the review of e013d95. Instead of just removing the comment like e013d95 did, let's add a more detailed comment. --- Source/Core/DiscIO/DirectoryBlob.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Core/DiscIO/DirectoryBlob.cpp b/Source/Core/DiscIO/DirectoryBlob.cpp index 332510efca93..d4f9e2bac75d 100644 --- a/Source/Core/DiscIO/DirectoryBlob.cpp +++ b/Source/Core/DiscIO/DirectoryBlob.cpp @@ -1252,6 +1252,9 @@ void DirectoryBlobPartition::WriteDirectory(std::vector* fst_data, std::move(content.m_source)); } + // For most files, an alignment of 0x20 is enough. But some files need 0x8000, + // in particular all DTK audio files in GameCube games. + // TODO: Do any Wii games need 0x8000 alignment? const u64 data_alignment = m_is_triforce ? 0x20ull : 0x8000ull; *data_offset = Common::AlignUp(*data_offset + entry.m_size, data_alignment); } From 900febec29bcfee8b7a1319ec74d9cb1bfb9d157 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 15 Jul 2026 02:12:31 -0400 Subject: [PATCH 131/270] IOS/ES: Prevent reading TMDs that are too small --- Source/Core/Core/IOS/ES/Formats.cpp | 2 +- Source/Core/Core/IOS/WFS/WFSI.cpp | 2 +- Source/Core/DiscIO/VolumeWad.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Core/Core/IOS/ES/Formats.cpp b/Source/Core/Core/IOS/ES/Formats.cpp index e055f72564b8..d0415141714d 100644 --- a/Source/Core/Core/IOS/ES/Formats.cpp +++ b/Source/Core/Core/IOS/ES/Formats.cpp @@ -194,7 +194,7 @@ void SignedBlobReader::DoState(PointerWrap& p) bool IsValidTMDSize(size_t size) { - return size <= 0x49e4; + return size >= sizeof(TMDHeader) && size <= 0x49e4; } TMDReader::TMDReader(std::vector bytes) : SignedBlobReader(std::move(bytes)) diff --git a/Source/Core/Core/IOS/WFS/WFSI.cpp b/Source/Core/Core/IOS/WFS/WFSI.cpp index c037967d6aca..73bf030dead1 100644 --- a/Source/Core/Core/IOS/WFS/WFSI.cpp +++ b/Source/Core/Core/IOS/WFS/WFSI.cpp @@ -156,7 +156,7 @@ std::optional WFSIDevice::IOCtl(const IOCtlRequest& request) if (!ES::IsValidTMDSize(tmd_size)) { - ERROR_LOG_FMT(IOS_WFS, "IOCTL_WFSI_IMPORT_TITLE_INIT: TMD size too large ({})", tmd_size); + ERROR_LOG_FMT(IOS_WFS, "IOCTL_WFSI_IMPORT_TITLE_INIT: TMD size invalid ({})", tmd_size); return_error_code = IPC_EINVAL; break; } diff --git a/Source/Core/DiscIO/VolumeWad.cpp b/Source/Core/DiscIO/VolumeWad.cpp index 39961a6c1853..974592fea0d3 100644 --- a/Source/Core/DiscIO/VolumeWad.cpp +++ b/Source/Core/DiscIO/VolumeWad.cpp @@ -51,7 +51,7 @@ VolumeWAD::VolumeWAD(std::unique_ptr reader) : m_reader(std::move(re if (!IOS::ES::IsValidTMDSize(m_tmd_size)) { - ERROR_LOG_FMT(DISCIO, "TMD is too large: {} bytes", m_tmd_size); + ERROR_LOG_FMT(DISCIO, "TMD has an invalid size: {} bytes", m_tmd_size); return; } From 83dc4685aacd52bb61cfcd9f3ffb74180d074cca Mon Sep 17 00:00:00 2001 From: cbartondock Date: Wed, 15 Jul 2026 12:02:16 -0700 Subject: [PATCH 132/270] waterfall loading of textures via id to prefer elfid-gameid when launched via elf; fallback to gameid if no match --- Source/Core/Core/ConfigManager.cpp | 35 ++++++++++++++-- Source/Core/Core/ConfigManager.h | 14 +++++++ .../DynamicInputTextureManager.cpp | 11 +++-- Source/Core/VideoCommon/HiresTextures.cpp | 40 ++++++++++++------- Source/Core/VideoCommon/HiresTextures.h | 7 ++++ 5 files changed, 83 insertions(+), 24 deletions(-) diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 460ca2e04885..32f74a030f1c 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -4,6 +4,7 @@ #include "Core/ConfigManager.h" #include +#include #include #include #include @@ -13,8 +14,6 @@ #include -#include - #include "AudioCommon/AudioCommon.h" #include "Common/CommonPaths.h" @@ -130,6 +129,27 @@ const std::string SConfig::GetGameTDBID() const return m_gametdb_id; } +const std::string SConfig::GetGameIDElfDol() const +{ + std::lock_guard lock(m_metadata_lock); + return m_game_id_elf_dol; +} + +const std::string SConfig::GetGameIDForTextures() const +{ + return GetGameIDsForTextures().front(); +} + +const std::vector SConfig::GetGameIDsForTextures() const +{ + std::lock_guard lock(m_metadata_lock); + + if (m_game_id_elf_dol.empty()) + return {m_game_id}; + + return {fmt::format("{}-{}", m_game_id_elf_dol, m_game_id), m_game_id}; +} + const std::string SConfig::GetTitleName() const { std::lock_guard lock(m_metadata_lock); @@ -164,6 +184,7 @@ void SConfig::ResetRunningGameMetadata() { std::lock_guard lock(m_metadata_lock); SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown, 0); + SetElfDolID(""); } void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume, @@ -269,6 +290,12 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri DolphinAnalytics::Instance().ReportGameStart(); } +void SConfig::SetElfDolID(const std::string& game_id) +{ + std::lock_guard lock(m_metadata_lock); + m_game_id_elf_dol = game_id; +} + void SConfig::OnESTitleChanged() { auto& system = Core::System::GetInstance(); @@ -368,7 +395,9 @@ struct SetGameMetadata constexpr char BACKSLASH = '\\'; constexpr char FORWARDSLASH = '/'; std::ranges::replace(executable_path, BACKSLASH, FORWARDSLASH); - config->SetRunningGameMetadata(SConfig::MakeGameID(PathToFileName(executable_path))); + const std::string made_game_id = SConfig::MakeGameID(PathToFileName(executable_path)); + config->SetRunningGameMetadata(made_game_id); + config->SetElfDolID(made_game_id); Host_TitleChanged(); diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index 145b00241801..039dcffd7a71 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "Common/CommonTypes.h" @@ -61,6 +62,16 @@ struct SConfig const std::string GetGameID() const; const std::string GetGameTDBID() const; + const std::string GetGameIDElfDol() const; + + // Returns a preference ordered list of ids to try for texture loading. + // If launched via elf/dol, returns {elfdolid-gameid, gameid}. Otherwise returns {gameid}. + const std::vector GetGameIDsForTextures() const; + + // Returns the single most-preferred id from GetGameIDsForTextures(); see that function for + // the full priority order. + const std::string GetGameIDForTextures() const; + const std::string GetTitleName() const; const std::string GetTitleDescription() const; u64 GetTitleID() const; @@ -71,6 +82,8 @@ struct SConfig void SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform); void SetRunningGameMetadata(const std::string& game_id); + void SetElfDolID(const std::string& game_id); + // Triggered when Dolphin loads a title directly // Reloads title-specific map files, patches, etc. static void OnTitleDirectlyBooted(const Core::CPUThreadGuard& guard); @@ -127,6 +140,7 @@ struct SConfig mutable std::recursive_mutex m_metadata_lock; std::string m_game_id; + std::string m_game_id_elf_dol; std::string m_gametdb_id; std::string m_title_name; std::string m_title_description; diff --git a/Source/Core/InputCommon/DynamicInputTextureManager.cpp b/Source/Core/InputCommon/DynamicInputTextureManager.cpp index be5c96340994..47d50524de94 100644 --- a/Source/Core/InputCommon/DynamicInputTextureManager.cpp +++ b/Source/Core/InputCommon/DynamicInputTextureManager.cpp @@ -23,10 +23,9 @@ DynamicInputTextureManager::~DynamicInputTextureManager() = default; void DynamicInputTextureManager::Load() { m_configuration.clear(); - - const std::string& game_id = SConfig::GetInstance().GetGameID(); const std::set dynamic_input_directories = - GetTextureDirectoriesWithGameId(File::GetUserPath(D_DYNAMICINPUT_IDX), game_id); + GetTextureDirectoriesForFirstMatchingGameId(File::GetUserPath(D_DYNAMICINPUT_IDX), + SConfig::GetInstance().GetGameIDsForTextures()); for (const auto& dynamic_input_directory : dynamic_input_directories) { @@ -46,8 +45,8 @@ void DynamicInputTextureManager::GenerateTextures(const Common::IniFile& file, { configuration.GenerateTextures(file, controller_names, &output); } - - const std::string& game_id = SConfig::GetInstance().GetGameID(); + // write to highest preference gameid for textures + const std::string& game_id_for_textures = SConfig::GetInstance().GetGameIDForTextures(); for (const auto& [generated_folder_name, images] : output) { const auto hi_res_folder = File::GetUserPath(D_HIRESTEXTURES_IDX) + generated_folder_name; @@ -63,7 +62,7 @@ void DynamicInputTextureManager::GenerateTextures(const Common::IniFile& file, File::CreateDir(game_id_folder); } - File::CreateEmptyFile(game_id_folder + DIR_SEP + game_id + ".txt"); + File::CreateEmptyFile(game_id_folder + DIR_SEP + game_id_for_textures + ".txt"); for (const auto& [image_name, image] : images) { diff --git a/Source/Core/VideoCommon/HiresTextures.cpp b/Source/Core/VideoCommon/HiresTextures.cpp index 1e423a8aa697..575f82be043b 100644 --- a/Source/Core/VideoCommon/HiresTextures.cpp +++ b/Source/Core/VideoCommon/HiresTextures.cpp @@ -3,6 +3,7 @@ #include "VideoCommon/HiresTextures.h" +#include #include #include #include @@ -10,8 +11,6 @@ #include #include -#include - #include "Common/CommonPaths.h" #include "Common/FileSearch.h" #include "Common/FileUtil.h" @@ -84,9 +83,9 @@ void HiresTexture::Update() return; } - const std::string& game_id = SConfig::GetInstance().GetGameID(); - const std::set texture_directories = - GetTextureDirectoriesWithGameId(File::GetUserPath(D_HIRESTEXTURES_IDX), game_id); + const std::set texture_directories = GetTextureDirectoriesForFirstMatchingGameId( + File::GetUserPath(D_HIRESTEXTURES_IDX), SConfig::GetInstance().GetGameIDsForTextures()); + constexpr auto extensions = std::to_array({".png", ".dds"}); for (const auto& texture_directory : texture_directories) @@ -141,16 +140,14 @@ void HiresTexture::Update() } } - if (g_ActiveConfig.bCacheHiresTextures) - { - OSD::AddMessage(fmt::format("Loading '{}' custom textures", s_hires_texture_cache.size()), - 10000); - } - else - { - OSD::AddMessage( - fmt::format("Found '{}' custom textures", s_hires_texture_id_to_arbmipmap.size()), 10000); - } + const std::vector game_ids_for_textures = + SConfig::GetInstance().GetGameIDsForTextures(); + const std::string game_id_display = fmt::format("{}", fmt::join(game_ids_for_textures, "' or '")); + + const auto message = fmt::format("{} '{}' custom textures for '{}'", + g_ActiveConfig.bCacheHiresTextures ? "Preloading" : "Found", + s_hires_texture_cache.size(), game_id_display); + OSD::AddMessage(message, 10000); } void HiresTexture::Clear() @@ -238,3 +235,16 @@ std::set GetTextureDirectoriesWithGameId(const std::string& root_di return result; } + +std::set +GetTextureDirectoriesForFirstMatchingGameId(const std::string& root_directory, + const std::vector& game_ids) +{ + for (const auto& game_id : game_ids) + { + auto directories = GetTextureDirectoriesWithGameId(root_directory, game_id); + if (!directories.empty()) + return directories; + } + return {}; +} diff --git a/Source/Core/VideoCommon/HiresTextures.h b/Source/Core/VideoCommon/HiresTextures.h index f577aebd786e..84c6c6b6467f 100644 --- a/Source/Core/VideoCommon/HiresTextures.h +++ b/Source/Core/VideoCommon/HiresTextures.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -20,6 +21,12 @@ enum class TextureFormat; std::set GetTextureDirectoriesWithGameId(const std::string& root_directory, const std::string& game_id); +// Tries each id in priority order, returning the directories for the first id that matches +// anything. Returns an empty set if none of the ids match. +std::set +GetTextureDirectoriesForFirstMatchingGameId(const std::string& root_directory, + const std::vector& game_ids); + class HiresTexture { public: From e58f57f6f3f7bc517887f5ef6d22741ff1af4900 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Fri, 17 Jul 2026 14:04:07 +0200 Subject: [PATCH 133/270] Auto assign wii controllers in netplay Just copy the gamecube logic and automatically assign players to the first empty slot. More consistent with GameCube and helps Android hosts where there is no UI for managing controller mappings currently. --- Source/Core/Core/NetPlayServer.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index 834544b6b3e1..d50fabba355f 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -2272,6 +2272,15 @@ void NetPlayServer::AssignNewUserAPad(const Client& player) break; } } + for (PlayerId& mapping : m_wiimote_map) + { + // 0 means unmapped + if (mapping == 0) + { + mapping = player.pid; + break; + } + } } PlayerId NetPlayServer::GiveFirstAvailableIDTo(ENetPeer* player) From 6877e436fbf1511bea91eeef569b94d0354771fc Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 12 May 2026 16:12:35 -0700 Subject: [PATCH 134/270] Android: Dual core warning when hosting netplay Using dual core during netplay seems sufficiently detrimental that its worth showing this warning message and the option to turn dual core off when starting netplay. --- .../netplay/model/NetplayViewModel.kt | 30 +++++- .../features/netplay/ui/NetplayActivity.kt | 3 + .../features/netplay/ui/NetplayScreen.kt | 100 +++++++++++++++++- .../features/settings/model/BooleanSetting.kt | 3 +- .../app/src/main/res/values/strings.xml | 3 + 5 files changed, 133 insertions(+), 6 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index adea1a13a53b..07644488240a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -11,15 +11,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.NetplaySession +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting @@ -104,18 +105,37 @@ class NetplayViewModel( private val _notAllPlayersHaveGame = Channel(Channel.CONFLATED) val notAllPlayersHaveGame = _notAllPlayersHaveGame.receiveAsFlow() + private val _dualCoreWarning = Channel(Channel.CONFLATED) + val dualCoreWarning = _dualCoreWarning.receiveAsFlow() + fun startGame() { - if (netplaySession.doAllPlayersHaveGame()) { - netplaySession.startGame() - } else { + if (!netplaySession.doAllPlayersHaveGame()) { _notAllPlayersHaveGame.trySend(Unit) + return + } + + if (BooleanSetting.MAIN_CPU_THREAD.boolean && + !BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.boolean + ) { + _dualCoreWarning.trySend(Unit) + return } + + confirmStartGame() } fun confirmStartGame() { netplaySession.startGame() } + fun setDualCoreEnabled(enabled: Boolean) { + BooleanSetting.MAIN_CPU_THREAD.setBoolean(NativeConfig.LAYER_BASE, enabled) + } + + fun skipDualCoreWarning() { + BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.setBoolean(NativeConfig.LAYER_BASE, true) + } + fun sendMessage(message: String) { val trimmedMessage = message.trim() if (trimmedMessage.isEmpty()) { @@ -176,12 +196,14 @@ class NetplayViewModel( JoinInfoType.EXTERNAL to JoinAddress.Loading, ) } + is TraversalState.Connected -> { _joinAddresses.value += mapOf( JoinInfoType.ROOM_ID to JoinAddress.Loaded(state.hostCode), JoinInfoType.EXTERNAL to JoinAddress.Loaded(state.externalAddress), ) } + is TraversalState.Failure -> { _joinAddresses.value += mapOf( JoinInfoType.ROOM_ID to JoinAddress.Unknown(retry), diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index eb9cecec206d..28e0e4ca16ec 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -58,6 +58,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { onGameSelected = viewModel::changeGame, gameFiles = viewModel.gameFiles.collectAsState().value, notAllPlayersHaveGame = viewModel.notAllPlayersHaveGame, + dualCoreWarning = viewModel.dualCoreWarning, + onSetDualCoreEnabled = viewModel::setDualCoreEnabled, + onSkipDualCoreWarning = viewModel::skipDualCoreWarning, onConfirmStartGame = viewModel::confirmStartGame, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 789a3cef1764..7df8ea35ee51 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -5,6 +5,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.Intent import android.content.res.Configuration import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,6 +38,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Remove import androidx.compose.material.icons.filled.Share @@ -59,6 +61,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetValue +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -72,7 +75,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -87,7 +93,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import androidx.window.core.layout.WindowSizeClass import coil.compose.AsyncImage @@ -129,6 +134,9 @@ fun NetplayScreen( onGameSelected: (GameFile) -> Unit, gameFiles: List, notAllPlayersHaveGame: Flow, + dualCoreWarning: Flow, + onSetDualCoreEnabled: (Boolean) -> Unit, + onSkipDualCoreWarning: () -> Unit, onConfirmStartGame: () -> Unit, hostInputAuthorityEnabled: Boolean, networkMode: NetworkMode, @@ -252,6 +260,11 @@ fun NetplayScreen( notAllPlayersHaveGame.collect { showNotAllPlayersHaveGame = true } } + var showDualCoreWarning by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(Unit) { + dualCoreWarning.collect { showDualCoreWarning = true } + } + var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) } if (saveTransferProgress == null) { dismissSaveTransferProgressDialog = false @@ -323,6 +336,21 @@ fun NetplayScreen( onDismissRequest = { showNotAllPlayersHaveGame = false }, ) } + + showDualCoreWarning -> { + DualCoreWarningDialog( + onSetDualCoreEnabled = onSetDualCoreEnabled, + onStartGame = { + showDualCoreWarning = false + onConfirmStartGame() + }, + onSkipWarning = { + showDualCoreWarning = false + onSkipDualCoreWarning() + }, + onDismiss = { showDualCoreWarning = false }, + ) + } } } } @@ -1273,6 +1301,73 @@ private fun GameDigestPlayerRow( } } +@Composable +private fun DualCoreWarningDialog( + onSetDualCoreEnabled: (Boolean) -> Unit, + onStartGame: () -> Unit, + onSkipWarning: () -> Unit, + onDismiss: () -> Unit, +) { + var dualCoreEnabled by rememberSaveable { mutableStateOf(true) } + AlertDialog( + title = { Text(stringResource(R.string.netplay_dual_core_warning_title)) }, + text = { + Column { + Text( + text = stringResource(R.string.netplay_dual_core_warning_message), + ) + val ripplePadding = 12.dp + Spacer(Modifier.height(12.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .layout { measurable, constraints -> + // Enlarge the ripple area beyond the dialog's padding + val hPx = ripplePadding.roundToPx() + val placeable = measurable.measure( + constraints.copy( + minWidth = constraints.maxWidth + hPx * 2, + maxWidth = constraints.maxWidth + hPx * 2 + ) + ) + layout(constraints.maxWidth, placeable.height) { + placeable.place(-hPx, 0) + } + } + .clip(MaterialTheme.shapes.small) + .clickable { + dualCoreEnabled = !dualCoreEnabled + onSetDualCoreEnabled(dualCoreEnabled) + } + .padding(ripplePadding), + ) { + Text( + text = stringResource(R.string.dual_core), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Switch( + checked = dualCoreEnabled, + onCheckedChange = null, + ) + } + } + }, + confirmButton = { + TextButton(onClick = onStartGame) { + Text(stringResource(R.string.netplay_start)) + } + }, + dismissButton = { + TextButton(onClick = onSkipWarning) { + Text(stringResource(R.string.netplay_dont_warn_again)) + } + }, + onDismissRequest = onDismiss, + ) +} + @Composable private fun NetplayMessage.color(): Color { val isDark = isSystemInDarkTheme() @@ -1361,6 +1456,9 @@ private fun PreviewNetplayScreen() { onGameSelected = {}, gameFiles = emptyList(), notAllPlayersHaveGame = emptyFlow(), + dualCoreWarning = emptyFlow(), + onSetDualCoreEnabled = {}, + onSkipDualCoreWarning = {}, onConfirmStartGame = {}, hostInputAuthorityEnabled = true, networkMode = NetworkMode.HOST_INPUT_AUTHORITY, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index 68c859082cd2..47f05f14ca2d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -947,7 +947,8 @@ enum class BooleanSetting( "ProgressEnabled", false ), - NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false); + NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false), + NETPLAY_SKIP_DUAL_CORE_WARNING(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "SkipDualCoreWarning", false); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 9d97169d951c..a6419ec83b18 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1012,6 +1012,9 @@ It can efficiently compress both junk data and encrypted Wii data. Start Warning Not all players have the game. Do you really want to start? + Dual Core Enabled + Dual core during netplay is not recommended. It can cause performance issues and prevent certain games from booting. + Don\'t warn again Chat Send Game changed to %1$s From 48b640c8bbdfa7b7a98875e1d47fbbb47e285390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Sun, 19 Jul 2026 23:46:56 +0200 Subject: [PATCH 135/270] GameFile: Disallow mods from being converted --- Source/Core/UICommon/GameFile.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index 4acab314d12d..bc623a841090 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -842,7 +842,8 @@ std::string GameFile::GetFileFormatName() const bool GameFile::ShouldAllowConversion() const { - return DiscIO::IsDisc(m_platform) && m_volume_size_type == DiscIO::DataSizeType::Accurate; + return DiscIO::IsDisc(m_platform) && m_volume_size_type == DiscIO::DataSizeType::Accurate && + !IsModDescriptor(); } bool GameFile::IsModDescriptor() const From 5b9961b0c6572dd15040c00ef7e0e244b5526028 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Sun, 19 Jul 2026 21:28:01 -0400 Subject: [PATCH 136/270] NetPlay: bound LZO decompression output NetPlay save synchronization decoded remote LZO blocks with the unsafe decoder and no output capacity. A malicious host could overflow a client buffer with a block larger than its declared size. Use the bounds-checking decoder, validate the declared output length, and grow buffer results only after each checked block has been decoded. --- Source/Core/Core/NetPlayCommon.cpp | 49 ++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/Source/Core/Core/NetPlayCommon.cpp b/Source/Core/Core/NetPlayCommon.cpp index 68500d04f4c6..0d1d73ecbf22 100644 --- a/Source/Core/Core/NetPlayCommon.cpp +++ b/Source/Core/Core/NetPlayCommon.cpp @@ -174,7 +174,7 @@ bool CompressBufferIntoPacket(std::span in_buffer, sf::Packet& packet) bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path) { - u64 file_size = Common::PacketReadU64(packet); + const u64 file_size = Common::PacketReadU64(packet); if (file_size == 0) return true; @@ -188,11 +188,12 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path) std::vector in_buffer(LZO_OUT_LEN); std::vector out_buffer(LZO_IN_LEN); + u64 bytes_written = 0; while (true) { - u32 cur_len = 0; // number of bytes to read - lzo_uint new_len = 0; // number of bytes to write + u32 cur_len = 0; // number of bytes to read + lzo_uint new_len = out_buffer.size(); // output buffer capacity packet >> cur_len; if (!cur_len) @@ -209,21 +210,29 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path) packet >> in_buffer[j]; } - if (lzo1x_decompress(in_buffer.data(), cur_len, out_buffer.data(), &new_len, nullptr) != + if (lzo1x_decompress_safe(in_buffer.data(), cur_len, out_buffer.data(), &new_len, nullptr) != LZO_E_OK) { PanicAlertFmtT("Internal LZO Error - decompression failed"); return false; } + if (new_len > file_size - bytes_written) + { + PanicAlertFmtT("LZO error - output is too large"); + return false; + } + if (!file.WriteBytes(out_buffer.data(), new_len)) { PanicAlertFmtT("Error writing file: {0}", file_path); return false; } + + bytes_written += new_len; } - return true; + return bytes_written == file_size; } static bool DecompressPacketIntoFolderInternal(sf::Packet& packet, const std::string& folder_path) @@ -269,20 +278,21 @@ bool DecompressPacketIntoFolder(sf::Packet& packet, const std::string& folder_pa std::optional> DecompressPacketIntoBuffer(sf::Packet& packet) { - u64 size = Common::PacketReadU64(packet); + const u64 size = Common::PacketReadU64(packet); - std::vector out_buffer(size); + std::vector out_buffer; if (size == 0) return out_buffer; std::vector in_buffer(LZO_OUT_LEN); + std::vector decompressed_buffer(LZO_IN_LEN); - lzo_uint i = 0; + u64 decompressed_size = 0; while (true) { - u32 cur_len = 0; // number of bytes to read - lzo_uint new_len = 0; // number of bytes to write + u32 cur_len = 0; // number of bytes to read + lzo_uint new_len = decompressed_buffer.size(); // output buffer capacity packet >> cur_len; if (!cur_len) @@ -299,13 +309,28 @@ std::optional> DecompressPacketIntoBuffer(sf::Packet& packet) packet >> in_buffer[j]; } - if (lzo1x_decompress(in_buffer.data(), cur_len, &out_buffer[i], &new_len, nullptr) != LZO_E_OK) + if (lzo1x_decompress_safe(in_buffer.data(), cur_len, decompressed_buffer.data(), &new_len, + nullptr) != LZO_E_OK) { PanicAlertFmtT("Internal LZO Error - decompression failed"); return {}; } - i += new_len; + if (new_len > size - decompressed_size || new_len > out_buffer.max_size() - out_buffer.size()) + { + PanicAlertFmtT("LZO error - output is too large"); + return {}; + } + + out_buffer.insert(out_buffer.end(), decompressed_buffer.begin(), + decompressed_buffer.begin() + new_len); + decompressed_size += new_len; + } + + if (decompressed_size != size) + { + PanicAlertFmtT("LZO error - output size mismatch"); + return {}; } return out_buffer; From 95ee7de40ef94facaf1123c128748e7f1fdf2149 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Mon, 20 Jul 2026 22:27:08 +0200 Subject: [PATCH 137/270] IOS/NetIPTopDevice: Zero-initialize sockaddr structs Fixes https://github.com/dolphin-emu/dolphin/security/advisories/GHSA-5fqv-9qrg-gm4j --- Source/Core/Core/IOS/Network/IP/Top.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/Core/IOS/Network/IP/Top.cpp b/Source/Core/Core/IOS/Network/IP/Top.cpp index 5040bfb7f85f..86d206d8e434 100644 --- a/Source/Core/Core/IOS/Network/IP/Top.cpp +++ b/Source/Core/Core/IOS/Network/IP/Top.cpp @@ -730,7 +730,7 @@ IPCReply NetIPTopDevice::HandleGetSockNameRequest(const IOCtlRequest& request) request.Log(GetDeviceName(), Common::Log::LogType::IOS_WC24); - sockaddr sa; + sockaddr sa{}; socklen_t sa_len = sizeof(sa); const int ret = getsockname(GetEmulationKernel().GetSocketManager()->GetHostSocket(fd), &sa, &sa_len); @@ -758,7 +758,7 @@ IPCReply NetIPTopDevice::HandleGetPeerNameRequest(const IOCtlRequest& request) u32 fd = memory.Read_U32(request.buffer_in); - sockaddr sa; + sockaddr sa{}; socklen_t sa_len = sizeof(sa); const int ret = getpeername(GetEmulationKernel().GetSocketManager()->GetHostSocket(fd), &sa, &sa_len); From 24aafdeb47b4723eda806fab68a0f9cd04ec9689 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 20 Jul 2026 19:50:54 -0400 Subject: [PATCH 138/270] Core: validate standalone ELF input ranges ElfReader trusted table offsets and counts from standalone ELF files. Malformed input could make it read and write past the loaded file buffer. Validate the ELF header, table ranges, segment data, section data, and string-table references before accessing them. Invalid files use the existing executable boot failure path. --- Source/Core/Core/Boot/ElfReader.cpp | 102 +++++++++++++++++++++++----- Source/Core/Core/Boot/ElfReader.h | 22 +++--- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/Source/Core/Core/Boot/ElfReader.cpp b/Source/Core/Core/Boot/ElfReader.cpp index f667387ab3cd..1a6e86e93e27 100644 --- a/Source/Core/Core/Boot/ElfReader.cpp +++ b/Source/Core/Core/Boot/ElfReader.cpp @@ -3,6 +3,7 @@ #include "Core/Boot/ElfReader.h" +#include #include #include @@ -70,62 +71,106 @@ static void byteswapSection(Elf32_Shdr& sec) ElfReader::ElfReader(std::vector buffer) : BootExecutableReader(std::move(buffer)) { - Initialize(m_bytes.data()); + m_is_valid = Initialize(); } ElfReader::ElfReader(File::IOFile file) : BootExecutableReader(std::move(file)) { - Initialize(m_bytes.data()); + m_is_valid = Initialize(); } ElfReader::ElfReader(const std::string& filename) : BootExecutableReader(filename) { - Initialize(m_bytes.data()); + m_is_valid = Initialize(); } ElfReader::~ElfReader() = default; -void ElfReader::Initialize(u8* ptr) +bool ElfReader::Initialize() { - base = (char*)ptr; - base32 = (u32*)ptr; - header = (Elf32_Ehdr*)ptr; + if (m_bytes.size() < sizeof(Elf32_Ehdr)) + return false; + + base = reinterpret_cast(m_bytes.data()); + base32 = reinterpret_cast(m_bytes.data()); + header = reinterpret_cast(m_bytes.data()); + if (header->e_ident[EI_MAG0] != ELFMAG0 || header->e_ident[EI_MAG1] != ELFMAG1 || + header->e_ident[EI_MAG2] != ELFMAG2 || header->e_ident[EI_MAG3] != ELFMAG3 || + header->e_ident[EI_CLASS] != ELFCLASS32 || header->e_ident[EI_DATA] != ELFDATA2MSB) + { + return false; + } + byteswapHeader(*header); - segments = (Elf32_Phdr*)(base + header->e_phoff); - sections = (Elf32_Shdr*)(base + header->e_shoff); + const auto is_range_valid = [this](size_t offset, size_t size) { + return offset <= m_bytes.size() && size <= m_bytes.size() - offset; + }; + if (header->e_ehsize != sizeof(Elf32_Ehdr) || + (header->e_phnum != 0 && header->e_phentsize != sizeof(Elf32_Phdr)) || + (header->e_shnum != 0 && header->e_shentsize != sizeof(Elf32_Shdr)) || + !is_range_valid(header->e_phoff, sizeof(Elf32_Phdr) * header->e_phnum) || + !is_range_valid(header->e_shoff, sizeof(Elf32_Shdr) * header->e_shnum) || + (header->e_shstrndx != SHN_UNDEF && header->e_shstrndx >= header->e_shnum)) + { + return false; + } + + segments = reinterpret_cast(base + header->e_phoff); + sections = reinterpret_cast(base + header->e_shoff); for (int i = 0; i < GetNumSegments(); i++) { byteswapSegment(segments[i]); + if (!is_range_valid(segments[i].p_offset, segments[i].p_filesz) || + segments[i].p_filesz > segments[i].p_memsz) + { + return false; + } } for (int i = 0; i < GetNumSections(); i++) { byteswapSection(sections[i]); + if (sections[i].sh_type != SHT_NOBITS && + !is_range_valid(sections[i].sh_offset, sections[i].sh_size)) + { + return false; + } } entryPoint = header->e_entry; bRelocate = (header->e_type != ET_EXEC); + return true; } const char* ElfReader::GetSectionName(int section) const { - if (sections[section].sh_type == SHT_NULL) + if (!m_is_valid || section < 0 || section >= header->e_shnum || + sections[section].sh_type == SHT_NULL) + { return nullptr; + } - int nameOffset = sections[section].sh_name; - char* ptr = (char*)GetSectionDataPtr(header->e_shstrndx); + const Elf32_Shdr& string_section = sections[header->e_shstrndx]; + const size_t name_offset = sections[section].sh_name; + const char* const ptr = reinterpret_cast(GetSectionDataPtr(header->e_shstrndx)); - if (ptr) - return ptr + nameOffset; - else + if (!ptr || name_offset >= string_section.sh_size || + !std::memchr(ptr + name_offset, '\0', string_section.sh_size - name_offset)) + { return nullptr; + } + + return ptr + name_offset; } // This is just a simple elf loader, good enough to load elfs generated by devkitPPC bool ElfReader::LoadIntoMemory(Core::System& system, bool only_in_mem1) const { + if (!m_is_valid) + return false; + INFO_LOG_FMT(BOOT, "String section: {}", header->e_shstrndx); if (bRelocate) @@ -183,15 +228,27 @@ SectionID ElfReader::GetSectionByName(const char* name, int firstSection) const bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_symbol_db, const std::string& filename) const { + if (!m_is_valid) + return false; + bool hasSymbols = false; SectionID sec = GetSectionByName(".symtab"); if (sec != -1) { - int stringSection = sections[sec].sh_link; - const char* stringBase = (const char*)GetSectionDataPtr(stringSection); + const u32 string_section_index = sections[sec].sh_link; + if (string_section_index >= header->e_shnum) + return false; + + const Elf32_Shdr& string_section = sections[string_section_index]; + const char* stringBase = (const char*)GetSectionDataPtr(string_section_index); + if (!stringBase) + return false; // We have a symbol table! Elf32_Sym* symtab = (Elf32_Sym*)(GetSectionDataPtr(sec)); + if (!symtab) + return false; + int numSymbols = sections[sec].sh_size / sizeof(Elf32_Sym); for (int sym = 0; sym < numSymbols; sym++) { @@ -203,7 +260,13 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_ int type = symtab[sym].st_info & 0xF; int sectionIndex = Common::swap16(symtab[sym].st_shndx); int value = Common::swap32(symtab[sym].st_value); - const char* name = stringBase + Common::swap32(symtab[sym].st_name); + const size_t name_offset = Common::swap32(symtab[sym].st_name); + if (name_offset >= string_section.sh_size || + !std::memchr(stringBase + name_offset, '\0', string_section.sh_size - name_offset)) + { + return false; + } + const char* name = stringBase + name_offset; if (bRelocate) value += sectionAddrs[sectionIndex]; @@ -229,6 +292,9 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_ bool ElfReader::IsWii() const { + if (!m_is_valid) + return false; + // Use the same method as the DOL loader uses: search for mfspr from HID4, // which should only be used in Wii ELFs. // diff --git a/Source/Core/Core/Boot/ElfReader.h b/Source/Core/Core/Boot/ElfReader.h index 83fd878012d2..cb6c8954964c 100644 --- a/Source/Core/Core/Boot/ElfReader.h +++ b/Source/Core/Core/Boot/ElfReader.h @@ -38,8 +38,7 @@ class ElfReader final : public BootExecutableReader bool LoadIntoMemory(Core::System& system, bool only_in_mem1 = false) const override; bool LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_symbol_db, const std::string& filename) const override; - // TODO: actually check for validity. - bool IsValid() const override { return true; } + bool IsValid() const override { return m_is_valid; } bool IsWii() const override; int GetNumSegments() const { return (int)(header->e_phnum); } @@ -65,16 +64,17 @@ class ElfReader final : public BootExecutableReader bool DidRelocate() const { return bRelocate; } private: - void Initialize(u8* bytes); + bool Initialize(); - char* base; - u32* base32; + char* base = nullptr; + u32* base32 = nullptr; - Elf32_Ehdr* header; - Elf32_Phdr* segments; - Elf32_Shdr* sections; + Elf32_Ehdr* header = nullptr; + Elf32_Phdr* segments = nullptr; + Elf32_Shdr* sections = nullptr; - u32* sectionAddrs; - bool bRelocate; - u32 entryPoint; + u32* sectionAddrs = nullptr; + bool bRelocate = false; + u32 entryPoint = 0; + bool m_is_valid = false; }; From 49453c045c3463a43a604b57a8abcff61063d8ad Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 20 Jul 2026 21:10:29 -0400 Subject: [PATCH 139/270] Core: log invalid ELF input Log each rejected ELF header, range, and symbol reference. This provides actionable diagnostics for malformed files without changing the validation behavior. --- Source/Core/Core/Boot/ElfReader.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Source/Core/Core/Boot/ElfReader.cpp b/Source/Core/Core/Boot/ElfReader.cpp index 1a6e86e93e27..8cdc1770a528 100644 --- a/Source/Core/Core/Boot/ElfReader.cpp +++ b/Source/Core/Core/Boot/ElfReader.cpp @@ -89,7 +89,10 @@ ElfReader::~ElfReader() = default; bool ElfReader::Initialize() { if (m_bytes.size() < sizeof(Elf32_Ehdr)) + { + ERROR_LOG_FMT(BOOT, "ELF file is too small."); return false; + } base = reinterpret_cast(m_bytes.data()); base32 = reinterpret_cast(m_bytes.data()); @@ -98,6 +101,7 @@ bool ElfReader::Initialize() header->e_ident[EI_MAG2] != ELFMAG2 || header->e_ident[EI_MAG3] != ELFMAG3 || header->e_ident[EI_CLASS] != ELFCLASS32 || header->e_ident[EI_DATA] != ELFDATA2MSB) { + ERROR_LOG_FMT(BOOT, "Invalid ELF header."); return false; } @@ -113,6 +117,7 @@ bool ElfReader::Initialize() !is_range_valid(header->e_shoff, sizeof(Elf32_Shdr) * header->e_shnum) || (header->e_shstrndx != SHN_UNDEF && header->e_shstrndx >= header->e_shnum)) { + ERROR_LOG_FMT(BOOT, "Invalid ELF header table."); return false; } @@ -125,6 +130,7 @@ bool ElfReader::Initialize() if (!is_range_valid(segments[i].p_offset, segments[i].p_filesz) || segments[i].p_filesz > segments[i].p_memsz) { + ERROR_LOG_FMT(BOOT, "Invalid ELF program header {}.", i); return false; } } @@ -135,6 +141,7 @@ bool ElfReader::Initialize() if (sections[i].sh_type != SHT_NOBITS && !is_range_valid(sections[i].sh_offset, sections[i].sh_size)) { + ERROR_LOG_FMT(BOOT, "Invalid ELF section header {}.", i); return false; } } @@ -237,17 +244,26 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_ { const u32 string_section_index = sections[sec].sh_link; if (string_section_index >= header->e_shnum) + { + ERROR_LOG_FMT(BOOT, "Invalid ELF symbol string table."); return false; + } const Elf32_Shdr& string_section = sections[string_section_index]; const char* stringBase = (const char*)GetSectionDataPtr(string_section_index); if (!stringBase) + { + ERROR_LOG_FMT(BOOT, "ELF symbol string table has no data."); return false; + } // We have a symbol table! Elf32_Sym* symtab = (Elf32_Sym*)(GetSectionDataPtr(sec)); if (!symtab) + { + ERROR_LOG_FMT(BOOT, "ELF symbol table has no data."); return false; + } int numSymbols = sections[sec].sh_size / sizeof(Elf32_Sym); for (int sym = 0; sym < numSymbols; sym++) @@ -264,6 +280,7 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_ if (name_offset >= string_section.sh_size || !std::memchr(stringBase + name_offset, '\0', string_section.sh_size - name_offset)) { + ERROR_LOG_FMT(BOOT, "Invalid ELF symbol name {}.", sym); return false; } const char* name = stringBase + name_offset; From d5d2f7c8fb417632bea23f1d6122f49db9cee6e4 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Wed, 22 Jul 2026 03:21:19 +0200 Subject: [PATCH 140/270] HW/DSPHLE/AXVoice: Check array bounds in ApplyUpdatesForMs() Fixes https://github.com/dolphin-emu/dolphin/security/advisories/GHSA-4q28-hhjv-hf3f --- Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h index 54531a85f635..1655e28074f3 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h +++ b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h @@ -65,12 +65,21 @@ void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdat for (int i = 0; i < curr_ms; ++i) start_idx += num_updates[i]; - for (u32 i = start_idx; i < start_idx + num_updates[curr_ms]; ++i) + if (start_idx < updates.size()) { - u16 update_off = updates[i].pb_offset; - u16 update_val = updates[i].new_value; + const u16 count = num_updates[curr_ms]; + if (count <= updates.size() - start_idx) + { + const u32 end_idx = start_idx + count; + for (u32 i = start_idx; i < end_idx; ++i) + { + const u16 update_off = updates[i].pb_offset; + const u16 update_val = updates[i].new_value; - pb_mem[update_off] = update_val; + if (update_off < pb_mem.size()) + pb_mem[update_off] = update_val; + } + } } pb = std::bit_cast(pb_mem); From 2ca2a5c8750b43fcd590fb9d9f9609cf0ab65f51 Mon Sep 17 00:00:00 2001 From: "Dr. Dystopia" Date: Wed, 22 Jul 2026 08:38:12 +0200 Subject: [PATCH 141/270] UICommon: Replace `find != std::npos` with `contains` --- Source/Core/UICommon/GameFile.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index 4acab314d12d..5385d75e7b66 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -630,9 +630,10 @@ std::string GameFile::GetNetPlayName(const Core::TitleDatabase& title_database) std::string lower_name = name; Common::ToLower(&lower_name); - if (disc_number > 1 && - lower_name.find(fmt::format("disc {}", disc_number)) == std::string::npos && - lower_name.find(fmt::format("disc{}", disc_number)) == std::string::npos) + auto is_numbered_disc = lower_name.contains(fmt::format("disc {}", disc_number)) || + lower_name.contains(fmt::format("disc{}", disc_number)); + + if (disc_number > 1 && !is_numbered_disc) { std::string disc_text = "Disc "; info.push_back(disc_text + std::to_string(disc_number)); From af38db4a238816dadb6a998f9d20808bab804159 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Wed, 22 Jul 2026 12:18:43 +0200 Subject: [PATCH 142/270] HW/DSPHLE/AXVoice: Prefer BitCastPtr over BitCastToArray in ApplyUpdatesForMs() --- Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h index 1655e28074f3..636c22ce65f5 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h +++ b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h @@ -59,8 +59,6 @@ PBUpdateData LoadPBUpdates(Memory::MemoryManager& memory, const PB_TYPE& pb) // Apply updates to a PB. void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdateData& updates) { - auto pb_mem = Common::BitCastToArray(pb); - u32 start_idx = 0; for (int i = 0; i < curr_ms; ++i) start_idx += num_updates[i]; @@ -76,13 +74,11 @@ void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdat const u16 update_off = updates[i].pb_offset; const u16 update_val = updates[i].new_value; - if (update_off < pb_mem.size()) - pb_mem[update_off] = update_val; + if (update_off < (sizeof(pb) / sizeof(u16))) + Common::BitCastPtr(&pb)[update_off] = update_val; } } } - - pb = std::bit_cast(pb_mem); } // Used to pass a large amount of buffers to the mixing function. From c490bc2c2e2377f198087756eef9c43195a698ef Mon Sep 17 00:00:00 2001 From: OatmealDome Date: Wed, 22 Jul 2026 17:11:18 -0400 Subject: [PATCH 143/270] BuildMacOSUniversalBinary: Add flag to enable CCache --- BuildMacOSUniversalBinary.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/BuildMacOSUniversalBinary.py b/BuildMacOSUniversalBinary.py index 3fc84a1c15f2..879b80ae09ff 100755 --- a/BuildMacOSUniversalBinary.py +++ b/BuildMacOSUniversalBinary.py @@ -69,6 +69,9 @@ # Whether our autoupdate functionality is enabled or not. "autoupdate": True, + # Whether CCache is used for the build or not. + "ccache": False, + # The distributor for this build. "distributor": "None" } @@ -119,6 +122,12 @@ def parse_args(conf=DEFAULT_CONFIG): action=argparse.BooleanOptionalAction, default=conf["autoupdate"]) + parser.add_argument( + "--ccache", + help="Enables CCache", + action=argparse.BooleanOptionalAction, + default=conf["ccache"]) + parser.add_argument( "--distributor", help="Sets the distributor for this build", @@ -304,7 +313,9 @@ def build(config): # iconv, bzip2, and curl "-DUSE_SYSTEM_ICONV=ON", "-DUSE_SYSTEM_BZIP2=ON", - "-DUSE_SYSTEM_CURL=ON" + "-DUSE_SYSTEM_CURL=ON", + "-DENABLE_CCACHE=" + + python_to_cmake_bool(config["ccache"]), ], env=env, cwd=arch) From a7f54272791efc53ee6a290c8a5cd1f5d129475c Mon Sep 17 00:00:00 2001 From: matellush <202132988+matellush@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:34:54 +0200 Subject: [PATCH 144/270] Flatpak: Update runtime to `6.11` --- Flatpak/org.DolphinEmu.dolphin-emu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flatpak/org.DolphinEmu.dolphin-emu.yml b/Flatpak/org.DolphinEmu.dolphin-emu.yml index a636a278e775..93435438f2e4 100644 --- a/Flatpak/org.DolphinEmu.dolphin-emu.yml +++ b/Flatpak/org.DolphinEmu.dolphin-emu.yml @@ -1,6 +1,6 @@ app-id: org.DolphinEmu.dolphin-emu runtime: org.kde.Platform -runtime-version: '6.10' +runtime-version: '6.11' sdk: org.kde.Sdk command: dolphin-emu-wrapper rename-desktop-file: dolphin-emu.desktop From 2eebf6fc2825400f1511d7c2d3d3d72bd9db32bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Fri, 24 Jul 2026 02:52:43 +0200 Subject: [PATCH 145/270] Flatpak: Use vendored-in SDL --- Flatpak/org.DolphinEmu.dolphin-emu.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flatpak/org.DolphinEmu.dolphin-emu.yml b/Flatpak/org.DolphinEmu.dolphin-emu.yml index 93435438f2e4..993a2ce9c488 100644 --- a/Flatpak/org.DolphinEmu.dolphin-emu.yml +++ b/Flatpak/org.DolphinEmu.dolphin-emu.yml @@ -53,6 +53,8 @@ modules: - -DENABLE_SDL=ON - -DENABLE_EVDEV=ON - -DDISTRIBUTOR=dolphin-emu.org + # Use the vendored-in SDL since the one from the SDK usually lags far behind + - -DUSE_SYSTEM_SDL3=OFF cleanup: - /share/man post-install: From d2833fb78882715950ab0a2c26c3ca20e3f6e349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Vanda=C3=ABle?= Date: Fri, 31 Oct 2025 12:57:16 +0100 Subject: [PATCH 146/270] Remove Visual Studio Projects and Solutions in favor of CMake --- Externals/Bochs_disasm/Bochs_disasm.vcxproj | 43 - Externals/Bochs_disasm/exports.props | 13 - Externals/FatFs/FatFs.vcxproj | 33 - Externals/FatFs/exports.props | 13 - Externals/FreeSurround/FreeSurround.vcxproj | 39 - .../FreeSurround/FreeSurround.vcxproj.filters | 42 - Externals/FreeSurround/exports.props | 13 - Externals/LZO/LZO.vcxproj | 35 - Externals/LZO/exports.props | 13 - Externals/SDL/SDL3.vcxproj | 533 ------ Externals/SDL/exports.props | 13 - Externals/SFML/SFML.vcxproj | 67 - Externals/SFML/exports.props | 14 - Externals/bzip2/bzip2.vcxproj | 34 - Externals/bzip2/bzip2.vcxproj.filters | 16 - Externals/bzip2/exports.props | 13 - Externals/cpp-ipc/cpp-ipc.vcxproj | 93 - Externals/cpp-ipc/exports.props | 13 - Externals/cpp-optparse/cpp-optparse.vcxproj | 27 - Externals/cpp-optparse/exports.props | 13 - Externals/cubeb/exports.props | 13 - Externals/cubeb/msvc/cubeb.vcxproj | 74 - Externals/cubeb/msvc/cubeb.vcxproj.filters | 62 - Externals/curl/curl.vcxproj | 390 ----- Externals/curl/exports.props | 17 - Externals/discord-rpc/exports.props | 13 - Externals/discord-rpc/src/discord-rpc.vcxproj | 44 - Externals/ed25519/ed25519.vcxproj | 42 - Externals/ed25519/ed25519.vcxproj.filters | 24 - Externals/ed25519/exports.props | 13 - Externals/enet/enet.vcxproj | 55 - Externals/enet/enet.vcxproj.filters | 55 - Externals/enet/exports.props | 13 - Externals/fmt/exports.props | 13 - Externals/fmt/fmt.vcxproj | 45 - Externals/glslang/exports.props | 26 - Externals/glslang/glslang.vcxproj | 97 -- Externals/glslang/glslang.vcxproj.filters | 2 - Externals/glslang/override.props | 37 - Externals/imgui/exports.props | 14 - Externals/imgui/imgui.vcxproj | 46 - Externals/implot/exports.props | 14 - Externals/implot/implot.vcxproj | 37 - Externals/liblzma/exports.props | 14 - Externals/liblzma/liblzma.vcxproj | 175 -- Externals/liblzma/liblzma.vcxproj.filters | 469 ------ Externals/libspng/exports.props | 14 - Externals/libspng/spng.vcxproj | 37 - Externals/libusb/exports.props | 13 - Externals/libusb/libusb-1.0.vcxproj | 54 - Externals/lz4/LZ4.vcxproj | 39 - Externals/lz4/exports.props | 13 - Externals/mGBA/exports.props | 14 - Externals/mGBA/mgba.vcxproj | 230 --- Externals/mGBA/mgba.vcxproj.filters | 451 ----- Externals/mbedtls/exports.props | 13 - Externals/mbedtls/mbedTLS.vcxproj | 131 -- Externals/miniupnpc/exports.props | 14 - Externals/miniupnpc/miniupnpc.vcxproj | 82 - Externals/minizip-ng/exports.props | 13 - Externals/minizip-ng/minizip-ng.vcxproj | 55 - Externals/picojson/exports.props | 13 - Externals/picojson/picojson.vcxproj | 27 - Externals/picojson/picojson.vcxproj.filters | 9 - Externals/pugixml/exports.props | 13 - Externals/pugixml/pugixml.vcxproj | 28 - Externals/rcheevos/exports.props | 13 - Externals/rcheevos/rcheevos.vcxproj | 88 - Externals/spirv_cross/exports.props | 14 - Externals/spirv_cross/spirv_cross.vcxproj | 53 - Externals/tinygltf/exports.props | 14 - Externals/tinygltf/tinygltf.vcxproj | 35 - Externals/xxhash/exports.props | 13 - Externals/xxhash/xxhash.vcxproj | 31 - Externals/zlib-ng/exports.props | 13 - Externals/zlib-ng/zlib-ng.vcxproj | 140 -- Externals/zstd/exports.props | 13 - Externals/zstd/zstd.vcxproj | 94 -- Externals/zstd/zstd.vcxproj.filters | 203 --- Languages/Languages.vcxproj | 33 - Languages/Languages.vcxproj.user | 6 - Source/Core/Common/SCMRevGen.vcxproj | 36 - Source/Core/Common/make_scmrev.h.js | 135 -- Source/Core/DolphinLib.ARM64.props | 35 - Source/Core/DolphinLib.props | 1491 ----------------- Source/Core/DolphinLib.vcxproj | 101 -- Source/Core/DolphinLib.vcxproj.user | 6 - Source/Core/DolphinLib.x64.props | 67 - Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj | 67 - .../DolphinNoGUI/DolphinNoGUI.vcxproj.filters | 21 - Source/Core/DolphinQt/DolphinQt.vcxproj | 534 ------ Source/Core/DolphinQt/DolphinQt.vcxproj.user | 8 - Source/Core/DolphinTool/DolphinTool.vcxproj | 73 - Source/Core/WinUpdater/WinUpdater.vcxproj | 58 - .../WinUpdater/WinUpdater.vcxproj.filters | 10 - Source/DSPSpy/DSPSpy.vcxproj | 180 -- Source/DSPSpy/DSPSpy.vcxproj.filters | 96 -- Source/DSPTool/DSPTool.vcxproj | 48 - Source/DSPTool/DSPTool.vcxproj.filters | 29 - Source/PCH/pch.vcxproj | 29 - Source/UnitTests/UnitTests.vcxproj | 129 -- Source/UnitTests/UnitTests.vcxproj.user | 9 - Source/VSProps/Base.Dolphin.props | 97 -- Source/VSProps/Base.Macros.props | 20 - Source/VSProps/Base.Targets.props | 24 - Source/VSProps/Base.props | 101 -- Source/VSProps/ClDisableAllWarnings.props | 11 - .../VSProps/Configuration.Application.props | 7 - Source/VSProps/Configuration.Base.props | 20 - .../VSProps/Configuration.StaticLibrary.props | 7 - Source/VSProps/Configuration.Utility.props | 7 - Source/VSProps/PCHCommon.props | 20 - Source/VSProps/PCHCreate.props | 36 - Source/VSProps/PCHUse.props | 22 - Source/VSProps/QtCompile.props | 152 -- Source/VSProps/qt_globals.targets | 558 ------ Source/VSProps/qt_tasks.targets | 1110 ------------ Source/VSProps/qtmoc.targets | 510 ------ Source/VSProps/qtmoc.xml | 5 - Source/VSProps/qtmoc_cl.targets | 49 - Source/VSProps/qtrcc.targets | 580 ------- Source/VSProps/qtrcc.xml | 5 - Source/dolphin-emu.sln | 495 ------ 123 files changed, 11908 deletions(-) delete mode 100644 Externals/Bochs_disasm/Bochs_disasm.vcxproj delete mode 100644 Externals/Bochs_disasm/exports.props delete mode 100644 Externals/FatFs/FatFs.vcxproj delete mode 100644 Externals/FatFs/exports.props delete mode 100644 Externals/FreeSurround/FreeSurround.vcxproj delete mode 100644 Externals/FreeSurround/FreeSurround.vcxproj.filters delete mode 100644 Externals/FreeSurround/exports.props delete mode 100644 Externals/LZO/LZO.vcxproj delete mode 100644 Externals/LZO/exports.props delete mode 100644 Externals/SDL/SDL3.vcxproj delete mode 100644 Externals/SDL/exports.props delete mode 100644 Externals/SFML/SFML.vcxproj delete mode 100644 Externals/SFML/exports.props delete mode 100644 Externals/bzip2/bzip2.vcxproj delete mode 100644 Externals/bzip2/bzip2.vcxproj.filters delete mode 100644 Externals/bzip2/exports.props delete mode 100644 Externals/cpp-ipc/cpp-ipc.vcxproj delete mode 100644 Externals/cpp-ipc/exports.props delete mode 100644 Externals/cpp-optparse/cpp-optparse.vcxproj delete mode 100644 Externals/cpp-optparse/exports.props delete mode 100644 Externals/cubeb/exports.props delete mode 100644 Externals/cubeb/msvc/cubeb.vcxproj delete mode 100644 Externals/cubeb/msvc/cubeb.vcxproj.filters delete mode 100644 Externals/curl/curl.vcxproj delete mode 100644 Externals/curl/exports.props delete mode 100644 Externals/discord-rpc/exports.props delete mode 100644 Externals/discord-rpc/src/discord-rpc.vcxproj delete mode 100644 Externals/ed25519/ed25519.vcxproj delete mode 100644 Externals/ed25519/ed25519.vcxproj.filters delete mode 100644 Externals/ed25519/exports.props delete mode 100644 Externals/enet/enet.vcxproj delete mode 100644 Externals/enet/enet.vcxproj.filters delete mode 100644 Externals/enet/exports.props delete mode 100644 Externals/fmt/exports.props delete mode 100644 Externals/fmt/fmt.vcxproj delete mode 100644 Externals/glslang/exports.props delete mode 100644 Externals/glslang/glslang.vcxproj delete mode 100644 Externals/glslang/glslang.vcxproj.filters delete mode 100644 Externals/glslang/override.props delete mode 100644 Externals/imgui/exports.props delete mode 100644 Externals/imgui/imgui.vcxproj delete mode 100644 Externals/implot/exports.props delete mode 100644 Externals/implot/implot.vcxproj delete mode 100644 Externals/liblzma/exports.props delete mode 100644 Externals/liblzma/liblzma.vcxproj delete mode 100644 Externals/liblzma/liblzma.vcxproj.filters delete mode 100644 Externals/libspng/exports.props delete mode 100644 Externals/libspng/spng.vcxproj delete mode 100644 Externals/libusb/exports.props delete mode 100644 Externals/libusb/libusb-1.0.vcxproj delete mode 100644 Externals/lz4/LZ4.vcxproj delete mode 100644 Externals/lz4/exports.props delete mode 100644 Externals/mGBA/exports.props delete mode 100644 Externals/mGBA/mgba.vcxproj delete mode 100644 Externals/mGBA/mgba.vcxproj.filters delete mode 100644 Externals/mbedtls/exports.props delete mode 100644 Externals/mbedtls/mbedTLS.vcxproj delete mode 100644 Externals/miniupnpc/exports.props delete mode 100644 Externals/miniupnpc/miniupnpc.vcxproj delete mode 100644 Externals/minizip-ng/exports.props delete mode 100644 Externals/minizip-ng/minizip-ng.vcxproj delete mode 100644 Externals/picojson/exports.props delete mode 100644 Externals/picojson/picojson.vcxproj delete mode 100644 Externals/picojson/picojson.vcxproj.filters delete mode 100644 Externals/pugixml/exports.props delete mode 100644 Externals/pugixml/pugixml.vcxproj delete mode 100644 Externals/rcheevos/exports.props delete mode 100644 Externals/rcheevos/rcheevos.vcxproj delete mode 100644 Externals/spirv_cross/exports.props delete mode 100644 Externals/spirv_cross/spirv_cross.vcxproj delete mode 100644 Externals/tinygltf/exports.props delete mode 100644 Externals/tinygltf/tinygltf.vcxproj delete mode 100644 Externals/xxhash/exports.props delete mode 100644 Externals/xxhash/xxhash.vcxproj delete mode 100644 Externals/zlib-ng/exports.props delete mode 100644 Externals/zlib-ng/zlib-ng.vcxproj delete mode 100644 Externals/zstd/exports.props delete mode 100644 Externals/zstd/zstd.vcxproj delete mode 100644 Externals/zstd/zstd.vcxproj.filters delete mode 100644 Languages/Languages.vcxproj delete mode 100644 Languages/Languages.vcxproj.user delete mode 100644 Source/Core/Common/SCMRevGen.vcxproj delete mode 100644 Source/Core/Common/make_scmrev.h.js delete mode 100644 Source/Core/DolphinLib.ARM64.props delete mode 100644 Source/Core/DolphinLib.props delete mode 100644 Source/Core/DolphinLib.vcxproj delete mode 100644 Source/Core/DolphinLib.vcxproj.user delete mode 100644 Source/Core/DolphinLib.x64.props delete mode 100644 Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj delete mode 100644 Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj.filters delete mode 100644 Source/Core/DolphinQt/DolphinQt.vcxproj delete mode 100644 Source/Core/DolphinQt/DolphinQt.vcxproj.user delete mode 100644 Source/Core/DolphinTool/DolphinTool.vcxproj delete mode 100644 Source/Core/WinUpdater/WinUpdater.vcxproj delete mode 100644 Source/Core/WinUpdater/WinUpdater.vcxproj.filters delete mode 100644 Source/DSPSpy/DSPSpy.vcxproj delete mode 100644 Source/DSPSpy/DSPSpy.vcxproj.filters delete mode 100644 Source/DSPTool/DSPTool.vcxproj delete mode 100644 Source/DSPTool/DSPTool.vcxproj.filters delete mode 100644 Source/PCH/pch.vcxproj delete mode 100644 Source/UnitTests/UnitTests.vcxproj delete mode 100644 Source/UnitTests/UnitTests.vcxproj.user delete mode 100644 Source/VSProps/Base.Dolphin.props delete mode 100644 Source/VSProps/Base.Macros.props delete mode 100644 Source/VSProps/Base.Targets.props delete mode 100644 Source/VSProps/Base.props delete mode 100644 Source/VSProps/ClDisableAllWarnings.props delete mode 100644 Source/VSProps/Configuration.Application.props delete mode 100644 Source/VSProps/Configuration.Base.props delete mode 100644 Source/VSProps/Configuration.StaticLibrary.props delete mode 100644 Source/VSProps/Configuration.Utility.props delete mode 100644 Source/VSProps/PCHCommon.props delete mode 100644 Source/VSProps/PCHCreate.props delete mode 100644 Source/VSProps/PCHUse.props delete mode 100644 Source/VSProps/QtCompile.props delete mode 100644 Source/VSProps/qt_globals.targets delete mode 100644 Source/VSProps/qt_tasks.targets delete mode 100644 Source/VSProps/qtmoc.targets delete mode 100644 Source/VSProps/qtmoc.xml delete mode 100644 Source/VSProps/qtmoc_cl.targets delete mode 100644 Source/VSProps/qtrcc.targets delete mode 100644 Source/VSProps/qtrcc.xml delete mode 100644 Source/dolphin-emu.sln diff --git a/Externals/Bochs_disasm/Bochs_disasm.vcxproj b/Externals/Bochs_disasm/Bochs_disasm.vcxproj deleted file mode 100644 index ca6e249b7d95..000000000000 --- a/Externals/Bochs_disasm/Bochs_disasm.vcxproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/Bochs_disasm/exports.props b/Externals/Bochs_disasm/exports.props deleted file mode 100644 index 7502e57cca4b..000000000000 --- a/Externals/Bochs_disasm/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)Bochs_disasm;%(AdditionalIncludeDirectories) - - - - - {8ada04d7-6db1-4da4-ab55-64fb12a0997b} - - - diff --git a/Externals/FatFs/FatFs.vcxproj b/Externals/FatFs/FatFs.vcxproj deleted file mode 100644 index 659f66083bfa..000000000000 --- a/Externals/FatFs/FatFs.vcxproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - {3F17D282-A77D-4931-B844-903AD0809A5E} - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Externals/FatFs/exports.props b/Externals/FatFs/exports.props deleted file mode 100644 index 3866879c432f..000000000000 --- a/Externals/FatFs/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)FatFs;%(AdditionalIncludeDirectories) - - - - - {3F17D282-A77D-4931-B844-903AD0809A5E} - - - diff --git a/Externals/FreeSurround/FreeSurround.vcxproj b/Externals/FreeSurround/FreeSurround.vcxproj deleted file mode 100644 index da3a11776abd..000000000000 --- a/Externals/FreeSurround/FreeSurround.vcxproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - {8498F2FA-5CA6-4169-9971-DE5B1FE6132C} - - - - - - - - - - - - - - include;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/FreeSurround/FreeSurround.vcxproj.filters b/Externals/FreeSurround/FreeSurround.vcxproj.filters deleted file mode 100644 index ade3fb0d19b2..000000000000 --- a/Externals/FreeSurround/FreeSurround.vcxproj.filters +++ /dev/null @@ -1,42 +0,0 @@ - - - - - source - - - source - - - source - - - source - - - - - include - - - include - - - include - - - include - - - include - - - - - {776ecb31-6d5e-489f-bac9-b91a1b202345} - - - {11345325-d67c-4a21-b2e9-c7c6c8cfc8b4} - - - \ No newline at end of file diff --git a/Externals/FreeSurround/exports.props b/Externals/FreeSurround/exports.props deleted file mode 100644 index af25a548b790..000000000000 --- a/Externals/FreeSurround/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)FreeSurround\include;%(AdditionalIncludeDirectories) - - - - - {8498f2fa-5ca6-4169-9971-de5b1fe6132c} - - - diff --git a/Externals/LZO/LZO.vcxproj b/Externals/LZO/LZO.vcxproj deleted file mode 100644 index df9855ccf9c0..000000000000 --- a/Externals/LZO/LZO.vcxproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - {AB993F38-C31D-4897-B139-A620C42BC565} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/LZO/exports.props b/Externals/LZO/exports.props deleted file mode 100644 index 815027470b6e..000000000000 --- a/Externals/LZO/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)LZO;%(AdditionalIncludeDirectories) - - - - - {ab993f38-c31d-4897-b139-a620c42bc565} - - - diff --git a/Externals/SDL/SDL3.vcxproj b/Externals/SDL/SDL3.vcxproj deleted file mode 100644 index d2515630ec14..000000000000 --- a/Externals/SDL/SDL3.vcxproj +++ /dev/null @@ -1,533 +0,0 @@ - - - - - - {8DC244EE-A0BD-4038-BAF7-CFAFA5EB2BAA} - - - - - - - - - - - - - - SDL\src;SDL\include;SDL\include\build_config;%(AdditionalIncludeDirectories) - SDL_AUDIO_DISABLED;SDL_VIDEO_DISABLED;SDL_GPU_DISABLED;SDL_RENDER_DISABLED;SDL_CAMERA_DISABLED;SDL_DIALOG_DISABLED;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Externals/SDL/exports.props b/Externals/SDL/exports.props deleted file mode 100644 index 3f4da20b8c90..000000000000 --- a/Externals/SDL/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)SDL\SDL\include;%(AdditionalIncludeDirectories) - - - - - {8DC244EE-A0BD-4038-BAF7-CFAFA5EB2BAA} - - - diff --git a/Externals/SFML/SFML.vcxproj b/Externals/SFML/SFML.vcxproj deleted file mode 100644 index 072677f94845..000000000000 --- a/Externals/SFML/SFML.vcxproj +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - {93D73454-2512-424E-9CDA-4BB357FE13DD} - - - - - - - - - - - - - - SFML\include;SFML\src;%(AdditionalIncludeDirectories) - SFML_STATIC;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/SFML/exports.props b/Externals/SFML/exports.props deleted file mode 100644 index 2f162883014f..000000000000 --- a/Externals/SFML/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)SFML\SFML\include;%(AdditionalIncludeDirectories) - SFML_STATIC;%(PreprocessorDefinitions) - - - - - {93d73454-2512-424e-9cda-4bb357fe13dd} - - - \ No newline at end of file diff --git a/Externals/bzip2/bzip2.vcxproj b/Externals/bzip2/bzip2.vcxproj deleted file mode 100644 index 28e3b2104ca2..000000000000 --- a/Externals/bzip2/bzip2.vcxproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - {1D8C51D2-FFA4-418E-B183-9F42B6A6717E} - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/bzip2/bzip2.vcxproj.filters b/Externals/bzip2/bzip2.vcxproj.filters deleted file mode 100644 index 97ad4ebde6a8..000000000000 --- a/Externals/bzip2/bzip2.vcxproj.filters +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/bzip2/exports.props b/Externals/bzip2/exports.props deleted file mode 100644 index c7bf7802e371..000000000000 --- a/Externals/bzip2/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)bzip2\bzip2;%(AdditionalIncludeDirectories) - - - - - {1d8c51d2-ffa4-418e-b183-9f42b6a6717e} - - - diff --git a/Externals/cpp-ipc/cpp-ipc.vcxproj b/Externals/cpp-ipc/cpp-ipc.vcxproj deleted file mode 100644 index 749f96f17418..000000000000 --- a/Externals/cpp-ipc/cpp-ipc.vcxproj +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - {7299DDD3-BBEC-4027-AF30-8DACC5415F96} - - - - - - - - - - - - - - cpp-ipc\include;cpp-ipc\src;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Externals/cpp-ipc/exports.props b/Externals/cpp-ipc/exports.props deleted file mode 100644 index bbcdc43b66ab..000000000000 --- a/Externals/cpp-ipc/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)cpp-ipc\cpp-ipc\include;%(AdditionalIncludeDirectories) - - - - - {7299DDD3-BBEC-4027-AF30-8DACC5415F96} - - - diff --git a/Externals/cpp-optparse/cpp-optparse.vcxproj b/Externals/cpp-optparse/cpp-optparse.vcxproj deleted file mode 100644 index ffe0dfd4c90e..000000000000 --- a/Externals/cpp-optparse/cpp-optparse.vcxproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - {C636D9D1-82FE-42B5-9987-63B7D4836341} - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/cpp-optparse/exports.props b/Externals/cpp-optparse/exports.props deleted file mode 100644 index efe9c290ed55..000000000000 --- a/Externals/cpp-optparse/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)cpp-optparse\cpp-optparse;%(AdditionalIncludeDirectories) - - - - - {c636d9d1-82fe-42b5-9987-63b7d4836341} - - - diff --git a/Externals/cubeb/exports.props b/Externals/cubeb/exports.props deleted file mode 100644 index bfea3d3abe54..000000000000 --- a/Externals/cubeb/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)cubeb\cubeb\include;$(ExternalsDir)cubeb\msvc;%(AdditionalIncludeDirectories) - - - - - {8ea11166-6512-44fc-b7a5-a4d1ecc81170} - - - diff --git a/Externals/cubeb/msvc/cubeb.vcxproj b/Externals/cubeb/msvc/cubeb.vcxproj deleted file mode 100644 index 9e054bbc3b4c..000000000000 --- a/Externals/cubeb/msvc/cubeb.vcxproj +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - {8EA11166-6512-44FC-B7A5-A4D1ECC81170} - - - - - - - - - - - - - - ..\cubeb\include;..\cubeb\src;..\msvc;..\cubeb\subprojects;%(AdditionalIncludeDirectories) - WIN32;USE_WASAPI;USE_WINMM;OUTSIDE_SPEEX;FLOATING_POINT;RANDOM_PREFIX=speex;EXPORT=;%(PreprocessorDefinitions) - - - - - NDEBUG;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/cubeb/msvc/cubeb.vcxproj.filters b/Externals/cubeb/msvc/cubeb.vcxproj.filters deleted file mode 100644 index fd366dbb06c0..000000000000 --- a/Externals/cubeb/msvc/cubeb.vcxproj.filters +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - speex - - - speex - - - speex - - - speex - - - speex - - - speex - - - speex - - - - - - - - - - - - - - speex - - - - - {caf7c7d0-0918-4299-8423-b287285d6fd0} - - - diff --git a/Externals/curl/curl.vcxproj b/Externals/curl/curl.vcxproj deleted file mode 100644 index 553c446bda6d..000000000000 --- a/Externals/curl/curl.vcxproj +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - {BB00605C-125F-4A21-B33B-7BF418322DCB} - - - - - - - - - - - - - - curl\include;curl\lib;%(AdditionalIncludeDirectories) - BUILDING_LIBCURL;CURL_STATICLIB;CURL_DISABLE_LDAP;USE_WINDOWS_SSPI;USE_SCHANNEL;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Externals/curl/exports.props b/Externals/curl/exports.props deleted file mode 100644 index 5b923bc635c5..000000000000 --- a/Externals/curl/exports.props +++ /dev/null @@ -1,17 +0,0 @@ - - - - - $(ExternalsDir)curl\curl\include;%(AdditionalIncludeDirectories) - CURL_STATICLIB;%(PreprocessorDefinitions) - - - secur32.lib;%(AdditionalDependencies) - - - - - {bb00605c-125f-4a21-b33b-7bf418322dcb} - - - diff --git a/Externals/discord-rpc/exports.props b/Externals/discord-rpc/exports.props deleted file mode 100644 index 7a4cfe28d9c0..000000000000 --- a/Externals/discord-rpc/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)discord-rpc\include;%(AdditionalIncludeDirectories) - - - - - {4482fd2a-ec43-3ffb-ac20-2e5c54b05ead} - - - diff --git a/Externals/discord-rpc/src/discord-rpc.vcxproj b/Externals/discord-rpc/src/discord-rpc.vcxproj deleted file mode 100644 index a3f1b38e7fe9..000000000000 --- a/Externals/discord-rpc/src/discord-rpc.vcxproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD} - - - - - - - - - - - - - - ..\include;..\thirdparty\include;%(AdditionalIncludeDirectories) - - - WIN32;_WINDOWS;DISCORD_WINDOWS;%(PreprocessorDefinitions) - ..\include;..\thirdparty\rapidjson-1.1.0\include;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - - diff --git a/Externals/ed25519/ed25519.vcxproj b/Externals/ed25519/ed25519.vcxproj deleted file mode 100644 index fa79d4deb9eb..000000000000 --- a/Externals/ed25519/ed25519.vcxproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - {5BDF4B91-1491-4FB0-BC27-78E9A8E97DC3} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/ed25519/ed25519.vcxproj.filters b/Externals/ed25519/ed25519.vcxproj.filters deleted file mode 100644 index d87c39cc1dcd..000000000000 --- a/Externals/ed25519/ed25519.vcxproj.filters +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/ed25519/exports.props b/Externals/ed25519/exports.props deleted file mode 100644 index c29d10c1807d..000000000000 --- a/Externals/ed25519/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)ed25519;%(AdditionalIncludeDirectories) - - - - - {5bdf4b91-1491-4fb0-bc27-78e9a8e97dc3} - - - diff --git a/Externals/enet/enet.vcxproj b/Externals/enet/enet.vcxproj deleted file mode 100644 index dfa606d91acd..000000000000 --- a/Externals/enet/enet.vcxproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - {CBC76802-C128-4B17-BF6C-23B08C313E5E} - - - - - - - - - - - - - - enet\include;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/enet/enet.vcxproj.filters b/Externals/enet/enet.vcxproj.filters deleted file mode 100644 index 792fcd606530..000000000000 --- a/Externals/enet/enet.vcxproj.filters +++ /dev/null @@ -1,55 +0,0 @@ - - - - - {a5756b80-36f2-45f6-b1f1-b67082477376} - - - - - include - - - include - - - include - - - include - - - include - - - include - - - include - - - include - - - include - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/enet/exports.props b/Externals/enet/exports.props deleted file mode 100644 index 1b2650e63d5c..000000000000 --- a/Externals/enet/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)enet\enet\include\;%(AdditionalIncludeDirectories) - - - - - {cbc76802-c128-4b17-bf6c-23b08c313e5e} - - - diff --git a/Externals/fmt/exports.props b/Externals/fmt/exports.props deleted file mode 100644 index 25e07f5e17fb..000000000000 --- a/Externals/fmt/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)fmt\fmt\include;%(AdditionalIncludeDirectories) - - - - - {4BC5A148-0AB3-440F-A980-A29B4B999190} - - - diff --git a/Externals/fmt/fmt.vcxproj b/Externals/fmt/fmt.vcxproj deleted file mode 100644 index 9a978b584893..000000000000 --- a/Externals/fmt/fmt.vcxproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - {4BC5A148-0AB3-440F-A980-A29B4B999190} - - - - - - - - - - - - - - fmt\include;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/glslang/exports.props b/Externals/glslang/exports.props deleted file mode 100644 index 1bc420fa2963..000000000000 --- a/Externals/glslang/exports.props +++ /dev/null @@ -1,26 +0,0 @@ - - - - - $(ExternalsDir)glslang;%(AdditionalIncludeDirectories) - $(ExternalsDir)glslang\glslang;%(AdditionalIncludeDirectories) - $(ExternalsDir)glslang\glslang\glslang;%(AdditionalIncludeDirectories) - $(ExternalsDir)glslang\glslang\glslang\Public;%(AdditionalIncludeDirectories) - $(ExternalsDir)glslang\glslang\SPIRV;%(AdditionalIncludeDirectories) - - - $(BuildRootDir)$(Platform)\$(Configuration)\glslang\bin\;%(AdditionalLibraryDirectories) - - - SPIRVd.lib;OSDependentd.lib;glslangd.lib;glslang-default-resource-limitsd.lib;GenericCodeGend.lib;MachineIndependentd.lib;%(AdditionalDependencies) - - - SPIRV.lib;OSDependent.lib;glslang.lib;glslang-default-resource-limits.lib;GenericCodeGen.lib;MachineIndependent.lib;%(AdditionalDependencies) - - - - - {d178061b-84d3-44f9-beed-efd18d9033f0} - - - diff --git a/Externals/glslang/glslang.vcxproj b/Externals/glslang/glslang.vcxproj deleted file mode 100644 index 867296c31e14..000000000000 --- a/Externals/glslang/glslang.vcxproj +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - {D178061B-84D3-44F9-BEED-EFD18D9033F0} - MakeFileProj - - - - - - Makefile - true - - - Makefile - false - - - - - - - - - - - - - "Visual Studio $(VisualStudioVersion.Substring(0,2))" - amd64 - arm64 - call vsdevcmd.bat -arch=$(DevCmdArch) - cmake -G $(CmakeGenerator) -A $(Platform) -T $(PlatformToolset) -DCMAKE_BUILD_TYPE="Release" -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DGLSLANG_TESTS=OFF -DENABLE_GLSLANG_BINARIES=OFF -DBUILD_EXTERNAL=OFF -DENABLE_SPVREMAPPER=OFF -DENABLE_HLSL=OFF -DENABLE_OPT=OFF -DENABLE_EXCEPTIONS=OFF -S glslang -B "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" - - call vsdevcmd.bat -arch=$(DevCmdArch) - cmake -G $(CmakeGenerator) -A $(Platform) -T $(PlatformToolset) -DCMAKE_BUILD_TYPE="Debug" -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug -DGLSLANG_TESTS=OFF -DENABLE_GLSLANG_BINARIES=OFF -DBUILD_EXTERNAL=OFF -DENABLE_SPVREMAPPER=OFF -DENABLE_HLSL=OFF -DENABLE_OPT=OFF -DENABLE_EXCEPTIONS=OFF -S glslang -B "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" - - - echo Copying.. - if not exist "$(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\bin\" mkdir -p "$(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\bin\" - copy "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)\SPIRV\$(CONFIGURATION)\*.lib" "$(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\bin" - copy "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)\glslang\OSDependent\Windows\$(CONFIGURATION)\*.lib" "$(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\bin" - copy "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)\glslang\$(CONFIGURATION)\*.lib" "$(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\bin" - - - echo Cleaning.. - rmdir /s /q "$(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\bin\" - rmdir /s /q "$(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)" - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\override.props')) - - - $(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\ - $(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)\ - - $(CmakeReleaseCLI) - msbuild.exe "$(IntDir)\ALL_BUILD.vcxproj" /t:build /p:Configuration=Release /p:ForceImportBeforeCppTargets="$(PropsAbsPath)" /m - $(CmakeCopyCLI) - - - $(CmakeReleaseCLI) - msbuild.exe "$(IntDir)\ALL_BUILD.vcxproj" /t:rebuild /p:Configuration=Release /p:ForceImportBeforeCppTargets="$(PropsAbsPath)" /m - $(CmakeCopyCLI) - - - $(CmakeReleaseCLI) - msbuild.exe "$(IntDir)\ALL_BUILD.vcxproj" /t:clean /p:Configuration=Release /p:ForceImportBeforeCppTargets="$(PropsAbsPath)" /m - $(CmakeCleanCLI) - - - - $(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\ - $(BuildRootDir)tmp\$(ProjectName)-$(Configuration)-$(Platform)\ - - $(CmakeDebugCLI) - msbuild.exe "$(IntDir)\ALL_BUILD.vcxproj" /t:build /p:Configuration=Debug /p:ForceImportBeforeCppTargets="$(PropsAbsPath)" /m - $(CmakeCopyCLI) - - - $(CmakeDebugCLI) - msbuild.exe "$(IntDir)\ALL_BUILD.vcxproj" /t:rebuild /p:Configuration=Debug /p:ForceImportBeforeCppTargets="$(PropsAbsPath)" /m - $(CmakeCopyCLI) - - - $(CmakeDebugCLI) - msbuild.exe "$(IntDir)\ALL_BUILD.vcxproj" /t:clean /p:Configuration=Debug /p:ForceImportBeforeCppTargets="$(PropsAbsPath)" /m - $(CmakeCleanCLI) - - - - - - - - diff --git a/Externals/glslang/glslang.vcxproj.filters b/Externals/glslang/glslang.vcxproj.filters deleted file mode 100644 index 94c470c09315..000000000000 --- a/Externals/glslang/glslang.vcxproj.filters +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Externals/glslang/override.props b/Externals/glslang/override.props deleted file mode 100644 index 065517b7b4c9..000000000000 --- a/Externals/glslang/override.props +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - 10.0 - - - - stdcpplatest - _SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING=1;_HAS_EXCEPTIONS=0;%(PreprocessorDefinitions) - false - - - - _DEBUG;%(PreprocessorDefinitions) - MultiThreadedDebugDLL - Disabled - - - Default - - - - AnySuitable - Speed - true - true - MultiThreadedDLL - false - /Gw /Zc:checkGwOdr %(AdditionalOptions) - true - - - - diff --git a/Externals/imgui/exports.props b/Externals/imgui/exports.props deleted file mode 100644 index bd4b7e546e67..000000000000 --- a/Externals/imgui/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)imgui;$(ExternalsDir)imgui\imgui;%(AdditionalIncludeDirectories) - IMGUI_USER_CONFIG="imgui_user_config.h";%(PreprocessorDefinitions) - - - - - {4c3b2264-ea73-4a7b-9cfe-65b0fd635ebb} - - - diff --git a/Externals/imgui/imgui.vcxproj b/Externals/imgui/imgui.vcxproj deleted file mode 100644 index 72b9f43f543f..000000000000 --- a/Externals/imgui/imgui.vcxproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - {4C3B2264-EA73-4A7B-9CFE-65B0FD635EBB} - - - - - - - - - - - - - - - .;imgui;$(CoreDir);%(AdditionalIncludeDirectories) - IMGUI_USER_CONFIG="imgui_user_config.h";%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/implot/exports.props b/Externals/implot/exports.props deleted file mode 100644 index 8c39fa095da8..000000000000 --- a/Externals/implot/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)imgui;%(AdditionalIncludeDirectories) - $(ExternalsDir)implot/implot;%(AdditionalIncludeDirectories) - - - - - {A608225E-AE0A-4D1A-9B55-97F57C862391} - - - diff --git a/Externals/implot/implot.vcxproj b/Externals/implot/implot.vcxproj deleted file mode 100644 index 11446581c633..000000000000 --- a/Externals/implot/implot.vcxproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - {A608225E-AE0A-4D1A-9B55-97F57C862391} - - - - - - - - - - - - - - - $(CoreDir);%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - diff --git a/Externals/liblzma/exports.props b/Externals/liblzma/exports.props deleted file mode 100644 index 2ab816c7a100..000000000000 --- a/Externals/liblzma/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)liblzma\api;%(AdditionalIncludeDirectories) - LZMA_API_STATIC;%(PreprocessorDefinitions) - - - - - {055a775f-b4f5-4970-9240-f6cf7661f37b} - - - diff --git a/Externals/liblzma/liblzma.vcxproj b/Externals/liblzma/liblzma.vcxproj deleted file mode 100644 index a4b7ff4a9d94..000000000000 --- a/Externals/liblzma/liblzma.vcxproj +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - {055A775F-B4F5-4970-9240-F6CF7661F37B} - - - - - - - - - - - - - - .;api;check;common;delta;lz;lzma;rangecoder;simple;tuklib;%(AdditionalIncludeDirectories) - LZMA_API_STATIC;HAVE_CONFIG_H;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/liblzma/liblzma.vcxproj.filters b/Externals/liblzma/liblzma.vcxproj.filters deleted file mode 100644 index b463cf7c7e32..000000000000 --- a/Externals/liblzma/liblzma.vcxproj.filters +++ /dev/null @@ -1,469 +0,0 @@ - - - - - api - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - api\lzma - - - check - - - check - - - check - - - check - - - check - - - check - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - delta - - - delta - - - delta - - - delta - - - lz - - - lz - - - lz - - - lz - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - rangecoder - - - rangecoder - - - rangecoder - - - rangecoder - - - simple - - - simple - - - simple - - - simple - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - - - - {2aae01e0-4f9a-4abc-b7e8-6622ea252cbe} - - - {32f50db1-5a2e-4f5b-bcf0-ffb43969287f} - - - {1a60cf76-1d90-40c7-b11d-a5c98f32895d} - - - {ce106295-7acd-4259-9254-5c3e801f11a9} - - - {36d95fac-7f84-42d2-a432-346d9f9d50e0} - - - {d4258d74-90ac-4e56-8075-7b2e8f24825f} - - - {b517cc3c-86c7-468f-97dd-86fb6d4a4316} - - - {36b5c9a4-420a-41d6-a0d5-c02255b55696} - - - {4f97f27d-2499-4492-ab35-edf73da89040} - - - {bd28534b-ffc6-4488-90b3-708ac608e76c} - - - - - check - - - check - - - check - - - check - - - check - - - check - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - delta - - - delta - - - delta - - - lz - - - lz - - - lz - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - lzma - - - rangecoder - - - simple - - - simple - - - simple - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - tuklib - - - \ No newline at end of file diff --git a/Externals/libspng/exports.props b/Externals/libspng/exports.props deleted file mode 100644 index ac255a65de98..000000000000 --- a/Externals/libspng/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)libspng\libspng\spng;%(AdditionalIncludeDirectories) - SPNG_STATIC;%(PreprocessorDefinitions) - - - - - {447B7B1E-1D74-4AEF-B2B9-6EB41C5D5313} - - - diff --git a/Externals/libspng/spng.vcxproj b/Externals/libspng/spng.vcxproj deleted file mode 100644 index 3a37bbaf5ace..000000000000 --- a/Externals/libspng/spng.vcxproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - {447B7B1E-1D74-4AEF-B2B9-6EB41C5D5313} - - - - - - - - - - - - - - libspng\spng;%(AdditionalIncludeDirectories) - SPNG_STATIC;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/libusb/exports.props b/Externals/libusb/exports.props deleted file mode 100644 index 3cca250f25e6..000000000000 --- a/Externals/libusb/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)libusb\libusb\libusb;%(AdditionalIncludeDirectories) - - - - - {349ee8f9-7d25-4909-aaf5-ff3fade72187} - - - diff --git a/Externals/libusb/libusb-1.0.vcxproj b/Externals/libusb/libusb-1.0.vcxproj deleted file mode 100644 index 5834338af97a..000000000000 --- a/Externals/libusb/libusb-1.0.vcxproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - {349EE8F9-7D25-4909-AAF5-FF3FADE72187} - - - - - - - - - - - - - - - libusb\msvc;libusb\libusb;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/lz4/LZ4.vcxproj b/Externals/lz4/LZ4.vcxproj deleted file mode 100644 index 36167718b1a0..000000000000 --- a/Externals/lz4/LZ4.vcxproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476} - - - - - - - - - - - - - - XXH_NAMESPACE=LZ4_;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/lz4/exports.props b/Externals/lz4/exports.props deleted file mode 100644 index 58d3c4d3ec42..000000000000 --- a/Externals/lz4/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)lz4\lz4\lib;%(AdditionalIncludeDirectories) - - - - - {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476} - - - diff --git a/Externals/mGBA/exports.props b/Externals/mGBA/exports.props deleted file mode 100644 index ddc8446c6d8b..000000000000 --- a/Externals/mGBA/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)mGBA\mgba\include;%(AdditionalIncludeDirectories) - ENABLE_VFS;ENABLE_DIRECTORIES;%(PreprocessorDefinitions) - - - - - {864C4C8E-296D-3DBC-AD83-F1D5CB6E8EC6} - - - diff --git a/Externals/mGBA/mgba.vcxproj b/Externals/mGBA/mgba.vcxproj deleted file mode 100644 index 026b9b9267ce..000000000000 --- a/Externals/mGBA/mgba.vcxproj +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - {864C4C8E-296D-3DBC-AD83-F1D5CB6E8EC6} - - - - - - - - - - - - - - mgba\include;mgba\src;mgba\src\third-party\lzma;%(AdditionalIncludeDirectories) - BUILD_STATIC;M_CORE_GB;M_CORE_GBA;USE_LZMA;_7ZIP_PPMD_SUPPPORT;HAVE_STRDUP;HAVE_SETLOCALE;HAVE_CHMOD;HAVE_UMASK;HAVE_CRC32;ENABLE_VFS;ENABLE_VFS_FD;ENABLE_DIRECTORIES;%(PreprocessorDefinitions) - - - "$(CScript)" /nologo /E:JScript "make_version.c.js" - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(IntDir)obj2\ - - - $(IntDir)obj2\ - - - - $(IntDir)obj2\ - - - - - - - $(IntDir)obj2\ - - - - - $(IntDir)obj2\ - - - - - $(IntDir)obj2\ - - - $(IntDir)obj2\ - - - - - - - - - $(IntDir)obj2\ - - - - - - $(IntDir)obj2\ - - - - - - - - - $(IntDir)obj3\ - - - - - - $(IntDir)obj3\ - - - - - - $(IntDir)obj3\ - - - $(IntDir)obj2\ - - - $(IntDir)obj2\ - - - $(IntDir)obj2\ - - - $(IntDir)obj3\ - - - - - - - - - - $(IntDir)obj3\ - - - - $(IntDir)obj2\ - - - - $(IntDir)obj2\ - - - $(IntDir)obj2\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(IntDir)obj3\ - - - - - - - - - - - - - - - - - - - - - - - - - $(IntDir)obj2\ - - - $(IntDir)obj3\ - - - - - - $(IntDir)obj2\ - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/mGBA/mgba.vcxproj.filters b/Externals/mGBA/mgba.vcxproj.filters deleted file mode 100644 index f3080a18e7dd..000000000000 --- a/Externals/mGBA/mgba.vcxproj.filters +++ /dev/null @@ -1,451 +0,0 @@ - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Generated sources - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Windows-specific code - - - Third-party code - - - Third-party code - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Virtual files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - {57438DCC-46E8-3FBA-90F2-185F80CEBE2C} - - - {C0CFD641-7357-3B1D-B2A3-B2477AEF3147} - - - {6C07F537-79D5-3651-A634-9E523B9936B2} - - - {AFF59D0C-C624-393F-8703-2FB3784928C8} - - - {37E5D4D5-B263-3B94-8968-21228F26DF67} - - - diff --git a/Externals/mbedtls/exports.props b/Externals/mbedtls/exports.props deleted file mode 100644 index e078d2e386fb..000000000000 --- a/Externals/mbedtls/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)mbedtls\include;%(AdditionalIncludeDirectories) - - - - - {bdb6578b-0691-4e80-a46c-df21639fd3b8} - - - diff --git a/Externals/mbedtls/mbedTLS.vcxproj b/Externals/mbedtls/mbedTLS.vcxproj deleted file mode 100644 index bb8f86cae9d8..000000000000 --- a/Externals/mbedtls/mbedTLS.vcxproj +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - {BDB6578B-0691-4E80-A46C-DF21639FD3B8} - - - - - - - - - - - - - - include;library;%(AdditionalIncludeDirectories) - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/miniupnpc/exports.props b/Externals/miniupnpc/exports.props deleted file mode 100644 index 09fd4b456e51..000000000000 --- a/Externals/miniupnpc/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)miniupnpc\miniupnp\miniupnpc\include;%(AdditionalIncludeDirectories) - MINIUPNP_STATICLIB;%(PreprocessorDefinitions) - - - - - {31643fdb-1bb8-4965-9de7-000fc88d35ae} - - - diff --git a/Externals/miniupnpc/miniupnpc.vcxproj b/Externals/miniupnpc/miniupnpc.vcxproj deleted file mode 100644 index 72fa14de1037..000000000000 --- a/Externals/miniupnpc/miniupnpc.vcxproj +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - {31643FDB-1BB8-4965-9DE7-000FC88D35AE} - - - - - - - - - - - - - - miniupnp\miniupnpc;miniupnp\miniupnpc\include;%(AdditionalIncludeDirectories) - MINIUPNP_STATICLIB;%(PreprocessorDefinitions) - - - - CD /d "$(ProjectDir)miniupnp\miniupnpc\msvc" - cscript //nologo genminiupnpcstrings.vbs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/minizip-ng/exports.props b/Externals/minizip-ng/exports.props deleted file mode 100644 index 799426edeab2..000000000000 --- a/Externals/minizip-ng/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)minizip-ng\minizip-ng;%(AdditionalIncludeDirectories) - - - - - {23114507-079a-4418-9707-cfa81a03ca99} - - - diff --git a/Externals/minizip-ng/minizip-ng.vcxproj b/Externals/minizip-ng/minizip-ng.vcxproj deleted file mode 100644 index 9f142dfb890d..000000000000 --- a/Externals/minizip-ng/minizip-ng.vcxproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - {23114507-079A-4418-9707-CFA81A03CA99} - - - - - - - - - - - - - - - minizip-ng;minizip-ng\compat;%(AdditionalIncludeDirectories) - HAVE_ZLIB;ZLIB_COMPAT;MZ_ZIP_NO_CRYPTO;MZ_ZIP_NO_ENCRYPTION;HAVE_STDINT_H;HAVE_INTTYPES_H;NO_FSEEKO;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/picojson/exports.props b/Externals/picojson/exports.props deleted file mode 100644 index 5cfcec591f91..000000000000 --- a/Externals/picojson/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)picojson;%(AdditionalIncludeDirectories) - - - - - {2c0d058e-de35-4471-ad99-e68a2caf9e18} - - - diff --git a/Externals/picojson/picojson.vcxproj b/Externals/picojson/picojson.vcxproj deleted file mode 100644 index ae446bd54a03..000000000000 --- a/Externals/picojson/picojson.vcxproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - {2C0D058E-DE35-4471-AD99-E68A2CAF9E18} - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/picojson/picojson.vcxproj.filters b/Externals/picojson/picojson.vcxproj.filters deleted file mode 100644 index 4abcb2be8f2e..000000000000 --- a/Externals/picojson/picojson.vcxproj.filters +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Externals/pugixml/exports.props b/Externals/pugixml/exports.props deleted file mode 100644 index bec44f13115d..000000000000 --- a/Externals/pugixml/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)pugixml\pugixml\src;%(AdditionalIncludeDirectories) - - - - - {38fee76f-f347-484b-949c-b4649381cffb} - - - diff --git a/Externals/pugixml/pugixml.vcxproj b/Externals/pugixml/pugixml.vcxproj deleted file mode 100644 index b78d925718ab..000000000000 --- a/Externals/pugixml/pugixml.vcxproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - {38FEE76F-F347-484B-949C-B4649381CFFB} - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/rcheevos/exports.props b/Externals/rcheevos/exports.props deleted file mode 100644 index d5e26ded5e84..000000000000 --- a/Externals/rcheevos/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)rcheevos;%(AdditionalIncludeDirectories) - - - - - {CC99A910-3752-4465-95AA-7DC240D92A99} - - - diff --git a/Externals/rcheevos/rcheevos.vcxproj b/Externals/rcheevos/rcheevos.vcxproj deleted file mode 100644 index b331e7e5f557..000000000000 --- a/Externals/rcheevos/rcheevos.vcxproj +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - {CC99A910-3752-4465-95AA-7DC240D92A99} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RC_DISABLE_LUA;RCHEEVOS_URL_SSL;RC_CLIENT_SUPPORTS_HASH;RC_CLIENT_SUPPORTS_EXTERNAL;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_HASH_NO_ENCRYPTED;RC_HASH_NO_ROM;RC_HASH_NO_ZIP;%(PreprocessorDefinitions) - $(ProjectDir)rcheevos\include;%(AdditionalIncludeDirectories) - - - - - - \ No newline at end of file diff --git a/Externals/spirv_cross/exports.props b/Externals/spirv_cross/exports.props deleted file mode 100644 index 8b5b94369006..000000000000 --- a/Externals/spirv_cross/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)spirv_cross\SPIRV-Cross;%(AdditionalIncludeDirectories) - SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS;%(PreprocessorDefinitions) - - - - - {3d780617-ec8c-4721-b9fd-dfc9bb658c7c} - - - diff --git a/Externals/spirv_cross/spirv_cross.vcxproj b/Externals/spirv_cross/spirv_cross.vcxproj deleted file mode 100644 index de93abda1b95..000000000000 --- a/Externals/spirv_cross/spirv_cross.vcxproj +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - {3d780617-ec8c-4721-b9fd-dfc9bb658c7c} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/tinygltf/exports.props b/Externals/tinygltf/exports.props deleted file mode 100644 index 7e2fde81cf9d..000000000000 --- a/Externals/tinygltf/exports.props +++ /dev/null @@ -1,14 +0,0 @@ - - - - - $(ExternalsDir)tinygltf;%(AdditionalIncludeDirectories) - TINYGLTF_NOEXCEPTION;TINYGLTF_NO_EXTERNAL_IMAGE;TINYGLTF_USE_CPP14;%(PreprocessorDefinitions) - - - - - {8bda3693-4999-4d11-9e52-8d08c30b643a} - - - diff --git a/Externals/tinygltf/tinygltf.vcxproj b/Externals/tinygltf/tinygltf.vcxproj deleted file mode 100644 index e33d949e56df..000000000000 --- a/Externals/tinygltf/tinygltf.vcxproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - {8bda3693-4999-4d11-9e52-8d08c30b643a} - - - - - - - - - - - - - - tinygltf;%(AdditionalIncludeDirectories) - - - - - - - - - - - - - - - diff --git a/Externals/xxhash/exports.props b/Externals/xxhash/exports.props deleted file mode 100644 index 6bcd16a46dd7..000000000000 --- a/Externals/xxhash/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)xxhash\xxHash\;%(AdditionalIncludeDirectories) - - - - - {677ea016-1182-440c-9345-dc88d1e98c0c} - - - diff --git a/Externals/xxhash/xxhash.vcxproj b/Externals/xxhash/xxhash.vcxproj deleted file mode 100644 index 3c51fd3ae6b8..000000000000 --- a/Externals/xxhash/xxhash.vcxproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - {677EA016-1182-440C-9345-DC88D1E98C0C} - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/zlib-ng/exports.props b/Externals/zlib-ng/exports.props deleted file mode 100644 index 0813bc25ab6a..000000000000 --- a/Externals/zlib-ng/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)zlib-ng;%(AdditionalIncludeDirectories) - - - - - {F6EA7144-8D64-4EBB-A13E-76DFBD911EAE} - - - diff --git a/Externals/zlib-ng/zlib-ng.vcxproj b/Externals/zlib-ng/zlib-ng.vcxproj deleted file mode 100644 index 1366e1da77b9..000000000000 --- a/Externals/zlib-ng/zlib-ng.vcxproj +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - {F6EA7144-8D64-4EBB-A13E-76DFBD911EAE} - - - - - - - - - - - - - - - .;zlib-ng;%(AdditionalIncludeDirectories) - ZLIB_COMPAT;WITH_GZFILEOP;NO_FSEEKO;%(PreprocessorDefinitions) - X86_FEATURES;X86_AVX2;X86_AVX2_ADLER32;X86_AVX_CHUNKSET;X86_AVX512;X86_AVX512_ADLER32;X86_MASK_INTRIN;X86_AVX512VNNI;X86_AVX512VNNI_ADLER32;X86_SSE41;X86_SSE42_CRC_HASH;X86_SSE42_ADLER32;X86_SSE42_CRC_INTRIN;X86_SSE2;X86_SSE2_CHUNKSET;X86_SSE2_SLIDEHASH;X86_SSSE3;X86_SSSE3_ADLER32;X86_PCLMULQDQ_CRC;X86_VPCLMULQDQ_CRC;%(PreprocessorDefinitions) - - ARM_FEATURES;ARM_NOCHECK_ACLE;ARM_ACLE_CRC_HASH;ARM_NEON;ARM_NEON_ADLER32;ARM_NEON_CHUNKSET;ARM_NEON_SLIDEHASH;__ARM_NEON__;ARM_NEON_HASLD4;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/zstd/exports.props b/Externals/zstd/exports.props deleted file mode 100644 index 2e193e0f5e75..000000000000 --- a/Externals/zstd/exports.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - $(ExternalsDir)zstd\zstd\lib;%(AdditionalIncludeDirectories) - - - - - {1bea10f3-80ce-4bc4-9331-5769372cdf99} - - - diff --git a/Externals/zstd/zstd.vcxproj b/Externals/zstd/zstd.vcxproj deleted file mode 100644 index 16473350e6cf..000000000000 --- a/Externals/zstd/zstd.vcxproj +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - {1BEA10F3-80CE-4BC4-9331-5769372CDF99} - - - - - - - - - - - - - - zstd\lib;%(AdditionalIncludeDirectories) - XXH_NAMESPACE=ZSTD_;ZSTD_LEGACY_SUPPORT=0;%(PreprocessorDefinitions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Externals/zstd/zstd.vcxproj.filters b/Externals/zstd/zstd.vcxproj.filters deleted file mode 100644 index 41a6c2320cad..000000000000 --- a/Externals/zstd/zstd.vcxproj.filters +++ /dev/null @@ -1,203 +0,0 @@ - - - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - decompress - - - decompress - - - decompress - - - decompress - - - - - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - common - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - compress - - - decompress - - - decompress - - - decompress - - - - - {e5afedd6-70b2-4294-b99d-e46a6ea9524c} - - - {09a75e83-2919-4d9e-9b05-26773de1e6bc} - - - {f84516ac-36c5-446d-a33f-58b78ba358f6} - - - \ No newline at end of file diff --git a/Languages/Languages.vcxproj b/Languages/Languages.vcxproj deleted file mode 100644 index 925884ea0435..000000000000 --- a/Languages/Languages.vcxproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - {0E033BE3-2E08-428E-9AE9-BC673EFA12B5} - - - - - - - - - - - - - - ..\Externals\gettext\msgfmt - - - - - - - - - - - - - \ No newline at end of file diff --git a/Languages/Languages.vcxproj.user b/Languages/Languages.vcxproj.user deleted file mode 100644 index 0f3d71122842..000000000000 --- a/Languages/Languages.vcxproj.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - true - - \ No newline at end of file diff --git a/Source/Core/Common/SCMRevGen.vcxproj b/Source/Core/Common/SCMRevGen.vcxproj deleted file mode 100644 index eea1446caa52..000000000000 --- a/Source/Core/Common/SCMRevGen.vcxproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - {41279555-F94F-4EBC-99DE-AF863C10C5C4} - - - - - - - - - - - - - - - - true - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/Common/make_scmrev.h.js b/Source/Core/Common/make_scmrev.h.js deleted file mode 100644 index b2ff644ecc75..000000000000 --- a/Source/Core/Common/make_scmrev.h.js +++ /dev/null @@ -1,135 +0,0 @@ -var wshShell = new ActiveXObject("WScript.Shell") -var oFS = new ActiveXObject("Scripting.FileSystemObject"); - -var outfile = "./scmrev.h"; -var cmd_revision = " rev-parse HEAD"; -var cmd_describe = " describe --always --long --dirty"; -var cmd_branch = " rev-parse --abbrev-ref HEAD"; -var cmd_commits_ahead = " rev-list --count HEAD ^master"; -var cmd_get_tag = " describe --exact-match HEAD"; - -function GetGitExe() -{ - try - { - gitexe = wshShell.RegRead("HKCU\\Software\\GitExtensions\\gitcommand"); - wshShell.Exec(gitexe); - return gitexe; - } - catch (e) - {} - - for (var gitexe in {"git.cmd":1, "git":1, "git.bat":1}) - { - try - { - wshShell.Exec(gitexe); - return gitexe; - } - catch (e) - {} - } - - // last try - msysgit not in path (vs2015 default) - msyspath = "\\Git\\cmd\\git.exe"; - gitexe = wshShell.ExpandEnvironmentStrings("%PROGRAMFILES(x86)%") + msyspath; - if (oFS.FileExists(gitexe)) { - return gitexe; - } - gitexe = wshShell.ExpandEnvironmentStrings("%PROGRAMFILES%") + msyspath; - if (oFS.FileExists(gitexe)) { - return gitexe; - } - - WScript.Echo("Cannot find git or git.cmd, check your PATH:\n" + - wshShell.ExpandEnvironmentStrings("%PATH%")); - WScript.Quit(1); -} - -function GetFirstStdOutLine(cmd) -{ - try - { - return wshShell.Exec(cmd).StdOut.ReadLine(); - } - catch (e) - { - // catch "the system cannot find the file specified" error - WScript.Echo("Failed to exec " + cmd + " this should never happen"); - WScript.Quit(1); - } -} - -function AttemptToExecuteCommand(cmd) -{ - try - { - var exec = wshShell.Exec(cmd) - - // wait until the command has finished - while (exec.Status == 0) {} - - return exec.ExitCode; - } - catch (e) - { - // catch "the system cannot find the file specified" error - WScript.Echo("Failed to exec " + cmd + " this should never happen"); - WScript.Quit(1); - } -} - -function GetFileContents(f) -{ - try - { - return oFS.OpenTextFile(f).ReadAll(); - } - catch (e) - { - // file doesn't exist - return ""; - } -} - -// get info from git -var gitexe = GetGitExe(); -var revision = GetFirstStdOutLine(gitexe + cmd_revision); -var describe = GetFirstStdOutLine(gitexe + cmd_describe); -var branch = GetFirstStdOutLine(gitexe + cmd_branch); -var commits_ahead = GetFirstStdOutLine(gitexe + cmd_commits_ahead); - -// Get environment information. -var distributor = wshShell.ExpandEnvironmentStrings("%DOLPHIN_DISTRIBUTOR%"); -if (distributor == "%DOLPHIN_DISTRIBUTOR%") distributor = "None"; -var default_update_track = wshShell.ExpandEnvironmentStrings("%DOLPHIN_DEFAULT_UPDATE_TRACK%"); -if (default_update_track == "%DOLPHIN_DEFAULT_UPDATE_TRACK%") default_update_track = ""; - -// remove hash (and trailing "-0" if needed) from description -describe = describe.replace(/(-0)?-[^-]+(-dirty)?$/, '$2'); - -// set commits ahead to zero if on a tag -if (AttemptToExecuteCommand(gitexe + cmd_get_tag) == 0) -{ - commits_ahead = "0"; -} - -var out_contents = - "#define SCM_REV_STR \"" + revision + "\"\n" + - "#define SCM_DESC_STR \"" + describe + "\"\n" + - "#define SCM_BRANCH_STR \"" + branch + "\"\n" + - "#define SCM_COMMITS_AHEAD_MASTER " + commits_ahead + "\n" + - "#define SCM_DISTRIBUTOR_STR \"" + distributor + "\"\n" + - "#define SCM_UPDATE_TRACK_STR \"" + default_update_track + "\"\n"; - -// check if file needs updating -if (out_contents == GetFileContents(outfile)) -{ - WScript.Echo(outfile + " current at " + describe); -} -else -{ - // needs updating - writeout current info - oFS.CreateTextFile(outfile, true).Write(out_contents); - WScript.Echo(outfile + " updated to " + describe); -} diff --git a/Source/Core/DolphinLib.ARM64.props b/Source/Core/DolphinLib.ARM64.props deleted file mode 100644 index 4ff1c4d91240..000000000000 --- a/Source/Core/DolphinLib.ARM64.props +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props deleted file mode 100644 index d343d4b15110..000000000000 --- a/Source/Core/DolphinLib.props +++ /dev/null @@ -1,1491 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj deleted file mode 100644 index 104b2d86edf1..000000000000 --- a/Source/Core/DolphinLib.vcxproj +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - {d79392f7-06d6-4b4b-a39f-4d587c215d3a} - - - - - - - - - - - - - - - - - - - - - - {41279555-f94f-4ebc-99de-af863c10c5c4} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Common\build_info.txt.in - $(BinaryOutputDir)build_info.txt - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/DolphinLib.vcxproj.user b/Source/Core/DolphinLib.vcxproj.user deleted file mode 100644 index 0f3d71122842..000000000000 --- a/Source/Core/DolphinLib.vcxproj.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - true - - \ No newline at end of file diff --git a/Source/Core/DolphinLib.x64.props b/Source/Core/DolphinLib.x64.props deleted file mode 100644 index fc9bb3fa65b0..000000000000 --- a/Source/Core/DolphinLib.x64.props +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj b/Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj deleted file mode 100644 index 7c11dac0dd79..000000000000 --- a/Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - {974E563D-23F8-4E8F-9083-F62876B04E08} - - - - - - - - - - - - - - - Console - - - - - {D79392F7-06D6-4B4B-A39F-4D587C215D3A} - - - {41279555-f94f-4ebc-99de-af863c10c5c4} - - - {0e033be3-2e08-428e-9ae9-bc673efa12b5} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj.filters b/Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj.filters deleted file mode 100644 index 91e0dceb569e..000000000000 --- a/Source/Core/DolphinNoGUI/DolphinNoGUI.vcxproj.filters +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj deleted file mode 100644 index dc2c53727b96..000000000000 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - {FA3FA62B-6F58-4B86-9453-4D149940A066} - Dolphin - - - - - - - - - - - - - - - $(ProjectDir)Config\Graphics;%(AdditionalIncludeDirectories) - $(ProjectDir)Config;%(AdditionalIncludeDirectories) - $(ProjectDir)Config\ControllerInterface;%(AdditionalIncludeDirectories) - $(ProjectDir)Config\Mapping;%(AdditionalIncludeDirectories) - $(ProjectDir)Debugger;%(AdditionalIncludeDirectories) - $(ProjectDir)FIFO;%(AdditionalIncludeDirectories) - $(ProjectDir)GameList;%(AdditionalIncludeDirectories) - $(ProjectDir)NetPlay;%(AdditionalIncludeDirectories) - $(ProjectDir)QtUtils;%(AdditionalIncludeDirectories) - $(ProjectDir)Settings;%(AdditionalIncludeDirectories) - $(ProjectDir)TAS;%(AdditionalIncludeDirectories) - $(ProjectDir)VideoInterface;%(AdditionalIncludeDirectories) - - _SILENCE_CXX23_ALIGNED_STORAGE_DEPRECATION_WARNING;%(PreprocessorDefinitions) - - $(SourceDir)PCH;%(AdditionalIncludeDirectories) - Use - pch_qt.h - pch_qt.h - - - DolphinQt.manifest;%(AdditionalManifestFiles) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {D79392F7-06D6-4B4B-A39F-4D587C215D3A} - - - {41279555-f94f-4ebc-99de-af863c10c5c4} - - - {0e033be3-2e08-428e-9ae9-bc673efa12b5} - - - - {76563A7F-1011-4EAD-B667-7BB18D09568E} - false - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj.user b/Source/Core/DolphinQt/DolphinQt.vcxproj.user deleted file mode 100644 index 22760f9a5ba2..000000000000 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj.user +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(BinaryOutputDir)$(TargetFileName) - WindowsLocalDebugger - true - - \ No newline at end of file diff --git a/Source/Core/DolphinTool/DolphinTool.vcxproj b/Source/Core/DolphinTool/DolphinTool.vcxproj deleted file mode 100644 index 79e1264b386a..000000000000 --- a/Source/Core/DolphinTool/DolphinTool.vcxproj +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - {8F91523C-5C5E-4B22-A1F1-67560B6DC714} - - - - - - - - - - - - - - - {D79392F7-06D6-4B4B-A39F-4D587C215D3A} - - - {41279555-f94f-4ebc-99de-af863c10c5c4} - - - {0e033be3-2e08-428e-9ae9-bc673efa12b5} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/Core/WinUpdater/WinUpdater.vcxproj b/Source/Core/WinUpdater/WinUpdater.vcxproj deleted file mode 100644 index 28ae61bb34f3..000000000000 --- a/Source/Core/WinUpdater/WinUpdater.vcxproj +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - {E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03} - - - - - - - - - - - - - - Updater - - - - {D79392F7-06D6-4B4B-A39F-4D587C215D3A} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/WinUpdater/WinUpdater.vcxproj.filters b/Source/Core/WinUpdater/WinUpdater.vcxproj.filters deleted file mode 100644 index d26f8a82cf21..000000000000 --- a/Source/Core/WinUpdater/WinUpdater.vcxproj.filters +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/Source/DSPSpy/DSPSpy.vcxproj b/Source/DSPSpy/DSPSpy.vcxproj deleted file mode 100644 index f3eb4620b596..000000000000 --- a/Source/DSPSpy/DSPSpy.vcxproj +++ /dev/null @@ -1,180 +0,0 @@ - - - - - GC - Win32 - - - GC - x64 - - - Wii - Win32 - - - Wii - x64 - - - - {3877AA3E-9CC0-4ADF-8E71-272EE4443478} - DSPSpy - - - - Makefile - MultiByte - true - - - Makefile - MultiByte - true - - - Makefile - MultiByte - true - - - Makefile - MultiByte - true - - - - - - - - - - - - - - - - - - - <_ProjectFileVersion>10.0.30319.1 - build\ - build\ - ..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_test.ds -make - - make clean -..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_code.ds -make - make clean - DSPSpy.dol - $(NMakePreprocessorDefinitions) - $(ProjectDir);C:\devkitPro\libogc\include;$(NMakeIncludeSearchPath) - $(NMakeForcedIncludes) - $(NMakeAssemblySearchPath) - $(NMakeForcedUsingAssemblies) - build\ - build\ - ..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_test.ds -make - - make clean -..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_code.ds -make - make clean - DSPSpy.dol - $(NMakePreprocessorDefinitions) - $(ProjectDir);C:\devkitPro\libogc\include;$(NMakeIncludeSearchPath) - $(NMakeForcedIncludes) - $(NMakeAssemblySearchPath) - $(NMakeForcedUsingAssemblies) - build\ - build\ - ..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_test.ds -make HW_TYPE=gamecube - make HW_TYPE=gamecube clean -..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_code.ds -make HW_TYPE=gamecube - make HW_TYPE=gamecube clean - DSPSpyGC.dol - $(NMakePreprocessorDefinitions) - $(ProjectDir);C:\devkitPro\libogc\include;$(NMakeIncludeSearchPath) - $(NMakeForcedIncludes) - $(NMakeAssemblySearchPath) - $(NMakeForcedUsingAssemblies) - build\ - build\ - ..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_test.ds -make HW_TYPE=gamecube - make HW_TYPE=gamecube clean -..\..\Binary\$(Platform)\DSPTool.exe -h dsp_code tests\dsp_code.ds -make HW_TYPE=gamecube - make HW_TYPE=gamecube clean - DSPSpyGC.dol - $(NMakePreprocessorDefinitions) - $(ProjectDir);C:\devkitPro\libogc\include;$(NMakeIncludeSearchPath) - $(NMakeForcedIncludes) - $(NMakeAssemblySearchPath) - $(NMakeForcedUsingAssemblies) - - - - build\BuildLog.htm - - - - - build\BuildLog.htm - - - - - build\BuildLogGC.htm - - - - - build\BuildLogGC.htm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/DSPSpy/DSPSpy.vcxproj.filters b/Source/DSPSpy/DSPSpy.vcxproj.filters deleted file mode 100644 index 5e52860f1dc7..000000000000 --- a/Source/DSPSpy/DSPSpy.vcxproj.filters +++ /dev/null @@ -1,96 +0,0 @@ - - - - - {14442340-8be2-4dcf-93f5-421cce23dd31} - - - {1a60828b-80d9-4b23-8b84-9116bf6e2630} - - - {ad8f68d7-73a6-4192-b188-125d2bef2a7a} - - - {251f77cb-8874-4bf1-bb2a-f7e6b009a0fb} - - - {80668e83-9986-4389-b587-21e9c6734589} - - - - - tests - - - tests - - - tests - - - tests - - - tests - - - tests - - - tests - - - tests - - - tests - - - tests - - - util - - - util - - - templates - - - templates - - - Misc uCodes - - - Misc uCodes - - - - - - - - DSP Interface - - - DSP Interface - - - - - - DSP Interface - - - DSP Interface - - - DSP Interface - - - - - - diff --git a/Source/DSPTool/DSPTool.vcxproj b/Source/DSPTool/DSPTool.vcxproj deleted file mode 100644 index 3cfb41b5fcbb..000000000000 --- a/Source/DSPTool/DSPTool.vcxproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - {1970D175-3DE8-4738-942A-4D98D1CDBF64} - - - - - - - - - - - - - - - Console - - - - - - - - - - - - {D79392F7-06D6-4B4B-A39F-4D587C215D3A} - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/DSPTool/DSPTool.vcxproj.filters b/Source/DSPTool/DSPTool.vcxproj.filters deleted file mode 100644 index d5c714c943a1..000000000000 --- a/Source/DSPTool/DSPTool.vcxproj.filters +++ /dev/null @@ -1,29 +0,0 @@ - - - - - {0b70242b-1d98-432f-a60e-d4ca0674852e} - - - - - TestData - - - TestData - - - TestData - - - TestData - - - - - - - - - - \ No newline at end of file diff --git a/Source/PCH/pch.vcxproj b/Source/PCH/pch.vcxproj deleted file mode 100644 index 1ee60c60c071..000000000000 --- a/Source/PCH/pch.vcxproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - {76563A7F-1011-4EAD-B667-7BB18D09568E} - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/UnitTests/UnitTests.vcxproj b/Source/UnitTests/UnitTests.vcxproj deleted file mode 100644 index 0cda54ff8846..000000000000 --- a/Source/UnitTests/UnitTests.vcxproj +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - {474661E7-C73A-43A6-AFEE-EE1EC433D49E} - - - - - - - - - - - - - - - - $(ExternalsDir)gtest\googletest\include;$(ExternalsDir)gtest\googletest;%(AdditionalIncludeDirectories) - - - Console - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {D79392F7-06D6-4B4B-A39F-4D587C215D3A} - - - {0e033be3-2e08-428e-9ae9-bc673efa12b5} - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/UnitTests/UnitTests.vcxproj.user b/Source/UnitTests/UnitTests.vcxproj.user deleted file mode 100644 index 7747d6682d6f..000000000000 --- a/Source/UnitTests/UnitTests.vcxproj.user +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - $(BinaryOutputDir) - WindowsLocalDebugger - - \ No newline at end of file diff --git a/Source/VSProps/Base.Dolphin.props b/Source/VSProps/Base.Dolphin.props deleted file mode 100644 index 93d58df5836c..000000000000 --- a/Source/VSProps/Base.Dolphin.props +++ /dev/null @@ -1,97 +0,0 @@ - - - - - $(ProjectName)$(TargetSuffix) - true - - - - - $(CoreDir);%(AdditionalIncludeDirectories) - - - $(ExternalsDir)FFmpeg-bin\$(Platform)\include;%(AdditionalIncludeDirectories) - $(ExternalsDir)OpenAL\include;%(AdditionalIncludeDirectories) - $(ExternalsDir)expr\include;%(AdditionalIncludeDirectories) - $(ExternalsDir)Vulkan-Headers\include;%(AdditionalIncludeDirectories) - $(ExternalsDir)VulkanMemoryAllocator\include;%(AdditionalIncludeDirectories) - $(ExternalsDir)watcher\watcher\include;%(AdditionalIncludeDirectories) - $(ExternalsDir)wil\include;%(AdditionalIncludeDirectories) - - WIL_SUPPRESS_EXCEPTIONS;%(PreprocessorDefinitions) - - WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions) - - - _CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_WARNINGS;%(PreprocessorDefinitions) - - _WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) - - _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;%(PreprocessorDefinitions) - - - _ARCH_64=1;_M_X86_64=1;%(PreprocessorDefinitions) - _ARCH_64=1;_M_ARM_64=1;%(PreprocessorDefinitions) - USE_UPNP;__LIBUSB__;%(PreprocessorDefinitions) - USE_ANALYTICS=1;%(PreprocessorDefinitions) - USE_DISCORD_PRESENCE;%(PreprocessorDefinitions) - HAVE_FFMPEG;%(PreprocessorDefinitions) - HAS_OPENGL;%(PreprocessorDefinitions) - HAS_VULKAN;%(PreprocessorDefinitions) - HAS_LIBMGBA;%(PreprocessorDefinitions) - AUTOUPDATE;%(PreprocessorDefinitions) - HAVE_SDL3;%(PreprocessorDefinitions) - USE_RETRO_ACHIEVEMENTS;%(PreprocessorDefinitions) - RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) - RC_CLIENT_SUPPORTS_RAINTEGRATION;%(PreprocessorDefinitions) - HAVE_CUBEB;%(PreprocessorDefinitions) - HAVE_CPPIPC;%(PreprocessorDefinitions) - - - 4201;4127;4100;4244;4121;4324;4714;%(DisableSpecificWarnings) - - 4245;%(DisableSpecificWarnings) - - /w44263 /w44265 /w44946 %(AdditionalOptions) - - - - avrt.lib;comctl32.lib;iphlpapi.lib;ksuser.lib;setupapi.lib;shlwapi.lib;winmm.lib;ws2_32.lib;%(AdditionalDependencies) - opengl32.lib;%(AdditionalDependencies) - - $(ExternalsDir)FFmpeg-bin\$(Platform)\lib;%(AdditionalLibraryDirectories) - avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;bcrypt.lib;%(AdditionalDependencies) - - Crypt32.lib;%(AdditionalDependencies) - - dwmapi.lib;%(AdditionalDependencies) - - enableCompatPatches - - - - - /NODEFAULTLIB:msvcrt %(AdditionalOptions) - - - diff --git a/Source/VSProps/Base.Macros.props b/Source/VSProps/Base.Macros.props deleted file mode 100644 index b2d936ed66e7..000000000000 --- a/Source/VSProps/Base.Macros.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - false - - D - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..'))\ - $(DolphinRootDir)Build\ - $(DolphinRootDir)Binary\ - $(BinaryRootDir)$(Platform)\ - $(DolphinRootDir)Externals\ - $(DolphinRootDir)Source\ - $(SourceDir)Core\ - %windir%\System32\cscript - $(SourceDir)VSProps\ - - - true - - \ No newline at end of file diff --git a/Source/VSProps/Base.Targets.props b/Source/VSProps/Base.Targets.props deleted file mode 100644 index 99c93fa3e0eb..000000000000 --- a/Source/VSProps/Base.Targets.props +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Debug - ARM64 - - - Debug - x64 - - - Release - ARM64 - - - Release - x64 - - - - 10.0 - - \ No newline at end of file diff --git a/Source/VSProps/Base.props b/Source/VSProps/Base.props deleted file mode 100644 index 2d22925d7b10..000000000000 --- a/Source/VSProps/Base.props +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - $(BuildRootDir)$(Platform)\$(Configuration)\$(ProjectName)\ - $(IntDir)bin\ - - false - - false - - - - - - TurnOffAllWarnings - true - true - true - - Level4 - true - true - false - true - true - - stdc17 - - stdcpplatest - false - - true - - /Zc:__cplusplus,enumTypes,externConstexpr,preprocessor,templateScope,throwingNew /volatile:iso %(AdditionalOptions) - - /Zo %(AdditionalOptions) - - /utf-8 %(AdditionalOptions) - OldStyle - Caret - - /experimental:deterministic %(AdditionalOptions) - - /pathmap:"$(DolphinRootDir)\"=d:\ %(AdditionalOptions) - /pathmap:"$(WindowsSdkDir)\"=w:\ %(AdditionalOptions) - /pathmap:"$(VCToolsetsDir)\"=v:\ %(AdditionalOptions) - - - - _DEBUG;%(PreprocessorDefinitions) - MultiThreadedDebugDLL - Disabled - - - Default - - - - AnySuitable - Speed - true - true - MultiThreadedDLL - false - /Gw /Zc:checkGwOdr %(AdditionalOptions) - true - - - - true - true - /experimental:deterministic %(AdditionalOptions) - - - - UseLinkTimeCodeGeneration - true - true - - - true - true - /experimental:deterministic %(AdditionalOptions) - - - - diff --git a/Source/VSProps/ClDisableAllWarnings.props b/Source/VSProps/ClDisableAllWarnings.props deleted file mode 100644 index bd683fd3bd7d..000000000000 --- a/Source/VSProps/ClDisableAllWarnings.props +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - TurnOffAllWarnings - - - - \ No newline at end of file diff --git a/Source/VSProps/Configuration.Application.props b/Source/VSProps/Configuration.Application.props deleted file mode 100644 index 8af23ba8a2b0..000000000000 --- a/Source/VSProps/Configuration.Application.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - Application - - \ No newline at end of file diff --git a/Source/VSProps/Configuration.Base.props b/Source/VSProps/Configuration.Base.props deleted file mode 100644 index d36fe43c8941..000000000000 --- a/Source/VSProps/Configuration.Base.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - v143 - Unicode - x64 - ARM64 - - - - - true - - - false - - \ No newline at end of file diff --git a/Source/VSProps/Configuration.StaticLibrary.props b/Source/VSProps/Configuration.StaticLibrary.props deleted file mode 100644 index 0da3b5006485..000000000000 --- a/Source/VSProps/Configuration.StaticLibrary.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - StaticLibrary - - \ No newline at end of file diff --git a/Source/VSProps/Configuration.Utility.props b/Source/VSProps/Configuration.Utility.props deleted file mode 100644 index d91a9836ba2b..000000000000 --- a/Source/VSProps/Configuration.Utility.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - Utility - - \ No newline at end of file diff --git a/Source/VSProps/PCHCommon.props b/Source/VSProps/PCHCommon.props deleted file mode 100644 index 6572d42fbdb2..000000000000 --- a/Source/VSProps/PCHCommon.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - pch.h - - $(BuildRootDir)$(Platform)\$(Configuration)\pch\pch.pch - - - \ No newline at end of file diff --git a/Source/VSProps/PCHCreate.props b/Source/VSProps/PCHCreate.props deleted file mode 100644 index c51d2803a6fd..000000000000 --- a/Source/VSProps/PCHCreate.props +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - Create - - - - - - - - - - - - $(BuildLibTargets);ClearLibCompiled - - - - false - - - - diff --git a/Source/VSProps/PCHUse.props b/Source/VSProps/PCHUse.props deleted file mode 100644 index 4bfb5fc67392..000000000000 --- a/Source/VSProps/PCHUse.props +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - Use - pch.h - - - - - {76563A7F-1011-4EAD-B667-7BB18D09568E} - false - true - true - - - \ No newline at end of file diff --git a/Source/VSProps/QtCompile.props b/Source/VSProps/QtCompile.props deleted file mode 100644 index bc3c4fe410a0..000000000000 --- a/Source/VSProps/QtCompile.props +++ /dev/null @@ -1,152 +0,0 @@ - - - - $(ExternalsDir)Qt\Qt6.8.3\ - $(ExternalsQtDir)$(Platform)\ - $(QtTargetDirDefault) - $(QTDIR)\ - false - true - $(QTDIR)include\ - $(QTDIR)lib\ - $(QTDIR)bin\ - $(QTDIR)plugins\ - $(IntDir) - d - $(ExternalsQtDir)x64\ - $(QtDebugSuffix) - QtPlugins - - - - QT_NO_DEBUG;%(PreprocessorDefinitions) - QT_DLL;QT_CORE_LIB;QT_GUI_LIB;QT_WIDGETS_LIB;%(PreprocessorDefinitions) - QT_USE_QSTRINGBUILDER;QT_NO_CAST_FROM_ASCII;QT_NO_CAST_TO_ASCII;%(PreprocessorDefinitions) - $(QtIncludeDir);%(AdditionalIncludeDirectories) - $(QtIncludeDir)QtCore;%(AdditionalIncludeDirectories) - $(QtIncludeDir)QtGui;%(AdditionalIncludeDirectories) - $(QtIncludeDir)QtWidgets;%(AdditionalIncludeDirectories) - - true - - - $(QtLibDir);%(AdditionalLibraryDirectories) - Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;%(AdditionalDependencies) - Windows - - - - - - - - - - - moc %(Identity) - $(QtHostToolsDir) - %(FullPath) - - $(QtToolOutDir)moc_68\moc_%(Filename).cpp - $(QtToolOutDir)moc_68\ - moc_%(Filename).cpp - output - true - [AllOptions] [AdditionalOptions] - %(OutputFile) - false - - - - - - - - - - - - rcc %(Identity) - $(QtHostToolsDir) - %(FullPath) - $(QtToolOutDir)rcc\qrc_%(Filename).cpp - $(QtToolOutDir)rcc\ - qrc_%(Filename).cpp - %(Filename) - default - false - output - true - [AllOptions] [AdditionalOptions] - %(OutputFile) - - - - - - - - - - - - - - - - - - - - - - $(BinaryOutputDir)qt.conf - - - - - - - - - - diff --git a/Source/VSProps/qt_globals.targets b/Source/VSProps/qt_globals.targets deleted file mode 100644 index 60abfacc3243..000000000000 --- a/Source/VSProps/qt_globals.targets +++ /dev/null @@ -1,558 +0,0 @@ - - - - - - - - - - QtVersion; - $(BuildDependsOn); - Qt - - - $(CleanDependsOn); - QtClean - - - $(DesignTimeBuildInitTargets); - Qt - - - $(ComputeCompileInputsTargets); - Qt - - - $(BeforeClCompileTargets); - Qt - - - $(ComputeLinkInputsTargets); - Qt - - - - - - - $(QtVSToolsVersion) - false - $([System.IO.Path]::Combine($(ProjectDir),$(IntDir)qt_work.log)) - $([System.Environment]::ProcessorCount) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $([System.String]::Copy('%(QtLogData.Identity)').Split('|')[1]) - - - - - - - - - - - - - - - - - @(QtWork->'%(WorkType)(%(Identity))') - @(QtWork->'%(DependenciesChanged)') - @(QtWork->'%(InputChanged)') - true - - - - - - - - @(log_entry->'%(Hash)') - - - - - true - true - - - - - - 0 - true - - - - - - - - - $(work_hash) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - InputPath - %(QtWork.Identity) - %(QtWork.Identity) - - - InputPath - @(ResourceFiles->'%(Value)') - @(ResourceFiles->'%(Value)') - - - OutputPath - %(QtWork.OutputFile) - %(QtWork.OutputFile) - - - - - - - - - - - - - - - - - - 0 - - - - - - - %(QtWorkResult.WorkType) - - - %(Identity) - $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','%(Identity)')) - - - - - - - - - %(QtWorkResult.WorkType) - - - %(Identity) - $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','%(Identity)')) - - - - - - - - - @(QtWorkResult, '|') - - - - - - - - - - - - - - - - - - - $(IntDir)%(Filename).o - - - - - - - - - - - - $(ProjectDir);%(AdditionalIncludeDirectories) - %(PrecompiledHeaderFile) - - - - - - - - - - - - - @(QtWorkResult->'%(ClCompile)') - - - $(SelectedFiles);$(SelectedClCompile) - - - - - - - - @(QtIncludePath->Distinct()) - - - - $(QtIncludePath);%(ClCompile.AdditionalIncludeDirectories) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(QtOuterBuildPrepare);QtOuterBuild;$(QtOuterBuildFinalize) - $(MSBuildProjectFullPath) - $([System.IO.Path]::GetRandomFileName()) - - - - - $(QtOuterBuildPrepare);$(QtOuterBuildFinalize) - - - diff --git a/Source/VSProps/qt_tasks.targets b/Source/VSProps/qt_tasks.targets deleted file mode 100644 index b87ccf039b94..000000000000 --- a/Source/VSProps/qt_tasks.targets +++ /dev/null @@ -1,1110 +0,0 @@ - - - - - - - - - - - - - - - - - - files = qrc - .Element("RCC") - .Elements("qresource") - .Elements("file"); - Uri QrcPath = new Uri(QrcFilePath); - Result = files - .Select(x => new Uri(QrcPath, x.Value).LocalPath) - .ToArray(); - ]]> - - - - - - - - - - - - - - - - - - x) - .Select(x => string.Format("[{0}={1}]", x, Item.GetMetadata(x)))) - .ToUpper()); - using (var dataZipped = new MemoryStream()) { - using (var zip = new DeflateStream(dataZipped, CompressionLevel.Fastest)) - zip.Write(data, 0, data.Length); - Hash = Convert.ToBase64String(dataZipped.ToArray()); - } - ]]> - - - - - - - - - -