From e02cc42d675ffe203c3f047f60669583934841ad Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:00:49 +0200 Subject: [PATCH 001/137] COS: Implement PPC va_list, va_arg and update related functions --- src/Cafe/OS/common/OSCommon.h | 83 +++++++++++++++ src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp | 112 ++++++++++---------- src/Cafe/OS/libs/coreinit/coreinit_Misc.h | 14 +++ src/Common/MemPtr.h | 19 ++-- src/Common/betype.h | 25 +++++ 5 files changed, 182 insertions(+), 71 deletions(-) diff --git a/src/Cafe/OS/common/OSCommon.h b/src/Cafe/OS/common/OSCommon.h index 4fb65a47..34f207bb 100644 --- a/src/Cafe/OS/common/OSCommon.h +++ b/src/Cafe/OS/common/OSCommon.h @@ -23,3 +23,86 @@ void osLib_returnFromFunction64(PPCInterpreter_t* hCPU, uint64 returnValue64); // utility functions #include "Cafe/OS/common/OSUtil.h" + +// va_list +struct ppc_va_list +{ + uint8be gprIndex; + uint8be fprIndex; + uint8be _padding2[2]; + MEMPTR overflow_arg_area; + MEMPTR reg_save_area; +}; +static_assert(sizeof(ppc_va_list) == 0xC); + +struct ppc_va_list_reg_storage +{ + uint32be gpr_save_area[8]; // 32 bytes, r3 to r10 + float64be fpr_save_area[8]; // 64 bytes, f1 to f8 + ppc_va_list vargs; + uint32be padding; +}; +static_assert(sizeof(ppc_va_list_reg_storage) == 0x70); + +// Equivalent of va_start for PPC HLE functions. Must be called before any StackAllocator<> definitions +#define ppc_define_va_list(__gprIndex, __fprIndex) \ + MPTR vaOriginalR1 = PPCInterpreter_getCurrentInstance()->gpr[1]; \ + StackAllocator va_list_storage; \ + for(int i=3; i<=10; i++) va_list_storage->gpr_save_area[i-3] = PPCInterpreter_getCurrentInstance()->gpr[i]; \ + for(int i=1; i<=8; i++) va_list_storage->fpr_save_area[i-1] = PPCInterpreter_getCurrentInstance()->fpr[i].fp0; \ + va_list_storage->vargs.gprIndex = __gprIndex; \ + va_list_storage->vargs.fprIndex = __fprIndex; \ + va_list_storage->vargs.reg_save_area = (uint8be*)&va_list_storage; \ + va_list_storage->vargs.overflow_arg_area = {vaOriginalR1 + 8}; \ + ppc_va_list& vargs = va_list_storage->vargs; + +enum class ppc_va_type +{ + INT32 = 1, + INT64 = 2, + FLOAT_OR_DOUBLE = 3, +}; + +static void* _ppc_va_arg(ppc_va_list* vargs, ppc_va_type argType) +{ + void* r; + switch ( argType ) + { + default: + cemu_assert_suspicious(); + case ppc_va_type::INT32: + if ( vargs[0].gprIndex < 8u ) + { + r = &vargs->reg_save_area[4 * vargs->gprIndex]; + vargs->gprIndex++; + return r; + } + r = vargs->overflow_arg_area; + vargs->overflow_arg_area += 4; + return r; + case ppc_va_type::INT64: + if ( (vargs->gprIndex & 1) != 0 ) + vargs->gprIndex++; + if ( vargs->gprIndex < 8 ) + { + r = &vargs->reg_save_area[4 * vargs->gprIndex]; + vargs->gprIndex += 2; + return r; + } + vargs->overflow_arg_area = {(vargs->overflow_arg_area.GetMPTR()+7) & 0xFFFFFFF8}; + r = vargs->overflow_arg_area; + vargs->overflow_arg_area += 8; + return r; + case ppc_va_type::FLOAT_OR_DOUBLE: + if ( vargs->fprIndex < 8 ) + { + r = &vargs->reg_save_area[0x20 + 8 * vargs->fprIndex]; + vargs->fprIndex++; + return r; + } + vargs->overflow_arg_area = {(vargs->overflow_arg_area.GetMPTR()+7) & 0xFFFFFFF8}; + r = vargs->overflow_arg_area; + vargs->overflow_arg_area += 8; + return r; + } +} \ No newline at end of file diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp index e2b50661..71a7d6e2 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp @@ -7,14 +7,9 @@ namespace coreinit { - - /* coreinit logging and string format */ - - sint32 ppcSprintf(const char* formatStr, char* strOut, sint32 maxLength, PPCInterpreter_t* hCPU, sint32 initialParamIndex) + sint32 ppc_vprintf(const char* formatStr, char* strOut, sint32 maxLength, ppc_va_list* vargs) { char tempStr[4096]; - sint32 integerParamIndex = initialParamIndex; - sint32 floatParamIndex = 0; sint32 writeIndex = 0; while (*formatStr) { @@ -101,8 +96,7 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU32(hCPU, integerParamIndex)); - integerParamIndex++; + sint32 tempLen = sprintf(tempStr, tempFormat, (uint32)*(uint32be*)_ppc_va_arg(vargs, ppc_va_type::INT32)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -120,13 +114,12 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - MPTR strOffset = PPCInterpreter_getCallParamU32(hCPU, integerParamIndex); + MPTR strOffset = *(uint32be*)_ppc_va_arg(vargs, ppc_va_type::INT32); sint32 tempLen = 0; if (strOffset == MPTR_NULL) tempLen = sprintf(tempStr, "NULL"); else tempLen = sprintf(tempStr, tempFormat, memory_getPointerFromVirtualOffset(strOffset)); - integerParamIndex++; for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -136,25 +129,6 @@ namespace coreinit } strOut[std::min(maxLength - 1, writeIndex)] = '\0'; } - else if (*formatStr == 'f') - { - // float - formatStr++; - strncpy(tempFormat, formatStart, std::min((std::ptrdiff_t)sizeof(tempFormat) - 1, formatStr - formatStart)); - if ((formatStr - formatStart) < sizeof(tempFormat)) - tempFormat[(formatStr - formatStart)] = '\0'; - else - tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, (float)hCPU->fpr[1 + floatParamIndex].fp0); - floatParamIndex++; - for (sint32 i = 0; i < tempLen; i++) - { - if (writeIndex >= maxLength) - break; - strOut[writeIndex] = tempStr[i]; - writeIndex++; - } - } else if (*formatStr == 'c') { // character @@ -164,8 +138,24 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU32(hCPU, integerParamIndex)); - integerParamIndex++; + sint32 tempLen = sprintf(tempStr, tempFormat, (uint32)*(uint32be*)_ppc_va_arg(vargs, ppc_va_type::INT32)); + for (sint32 i = 0; i < tempLen; i++) + { + if (writeIndex >= maxLength) + break; + strOut[writeIndex] = tempStr[i]; + writeIndex++; + } + } + else if (*formatStr == 'f' || *formatStr == 'g' || *formatStr == 'G') + { + formatStr++; + strncpy(tempFormat, formatStart, std::min((std::ptrdiff_t)sizeof(tempFormat) - 1, formatStr - formatStart)); + if ((formatStr - formatStart) < sizeof(tempFormat)) + tempFormat[(formatStr - formatStart)] = '\0'; + else + tempFormat[sizeof(tempFormat) - 1] = '\0'; + sint32 tempLen = sprintf(tempStr, tempFormat, (double)*(betype*)_ppc_va_arg(vargs, ppc_va_type::FLOAT_OR_DOUBLE)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -183,8 +173,7 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, (double)hCPU->fpr[1 + floatParamIndex].fp0); - floatParamIndex++; + sint32 tempLen = sprintf(tempStr, tempFormat, (double)*(betype*)_ppc_va_arg(vargs, ppc_va_type::FLOAT_OR_DOUBLE)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -196,16 +185,13 @@ namespace coreinit else if ((formatStr[0] == 'l' && formatStr[1] == 'l' && (formatStr[2] == 'x' || formatStr[2] == 'X'))) { formatStr += 3; - // double (64bit) + // 64bit int strncpy(tempFormat, formatStart, std::min((std::ptrdiff_t)sizeof(tempFormat) - 1, formatStr - formatStart)); if ((formatStr - formatStart) < sizeof(tempFormat)) tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - if (integerParamIndex & 1) - integerParamIndex++; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU64(hCPU, integerParamIndex)); - integerParamIndex += 2; + sint32 tempLen = sprintf(tempStr, tempFormat, (uint64)*(uint64be*)_ppc_va_arg(vargs, ppc_va_type::INT64)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -223,10 +209,7 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - if (integerParamIndex & 1) - integerParamIndex++; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU64(hCPU, integerParamIndex)); - integerParamIndex += 2; + sint32 tempLen = sprintf(tempStr, tempFormat, (sint64)*(sint64be*)_ppc_va_arg(vargs, ppc_va_type::INT64)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -255,9 +238,12 @@ namespace coreinit return std::min(writeIndex, maxLength - 1); } + /* coreinit logging and string format */ + sint32 __os_snprintf(char* outputStr, sint32 maxLength, const char* formatStr) { - sint32 r = ppcSprintf(formatStr, outputStr, maxLength, PPCInterpreter_getCurrentInstance(), 3); + ppc_define_va_list(3, 0); + sint32 r = ppc_vprintf(formatStr, outputStr, maxLength, &vargs); return r; } @@ -322,32 +308,40 @@ namespace coreinit } } - void OSReport(const char* format) + void COSVReport(COSReportModule module, COSReportLevel level, const char* format, ppc_va_list* vargs) { - char buffer[1024 * 2]; - sint32 len = ppcSprintf(format, buffer, sizeof(buffer), PPCInterpreter_getCurrentInstance(), 1); - WriteCafeConsole(CafeLogType::OSCONSOLE, buffer, len); + char tmpBuffer[1024]; + sint32 len = ppc_vprintf(format, tmpBuffer, sizeof(tmpBuffer), vargs); + WriteCafeConsole(CafeLogType::OSCONSOLE, tmpBuffer, len); } - void OSVReport(const char* format, MPTR vaArgs) + void OSReport(const char* format) { - cemu_assert_unimplemented(); + ppc_define_va_list(1, 0); + COSVReport(COSReportModule::coreinit, COSReportLevel::Info, format, &vargs); + } + + void OSVReport(const char* format, ppc_va_list* vargs) + { + COSVReport(COSReportModule::coreinit, COSReportLevel::Info, format, vargs); } void COSWarn(int moduleId, const char* format) { - char buffer[1024 * 2]; - int prefixLen = sprintf(buffer, "[COSWarn-%d] ", moduleId); - sint32 len = ppcSprintf(format, buffer + prefixLen, sizeof(buffer) - prefixLen, PPCInterpreter_getCurrentInstance(), 2); - WriteCafeConsole(CafeLogType::OSCONSOLE, buffer, len + prefixLen); + ppc_define_va_list(2, 0); + char tmpBuffer[1024]; + int prefixLen = sprintf(tmpBuffer, "[COSWarn-%d] ", moduleId); + sint32 len = ppc_vprintf(format, tmpBuffer + prefixLen, sizeof(tmpBuffer) - prefixLen, &vargs); + WriteCafeConsole(CafeLogType::OSCONSOLE, tmpBuffer, len + prefixLen); } void OSLogPrintf(int ukn1, int ukn2, int ukn3, const char* format) { - char buffer[1024 * 2]; - int prefixLen = sprintf(buffer, "[OSLogPrintf-%d-%d-%d] ", ukn1, ukn2, ukn3); - sint32 len = ppcSprintf(format, buffer + prefixLen, sizeof(buffer) - prefixLen, PPCInterpreter_getCurrentInstance(), 4); - WriteCafeConsole(CafeLogType::OSCONSOLE, buffer, len + prefixLen); + ppc_define_va_list(4, 0); + char tmpBuffer[1024]; + int prefixLen = sprintf(tmpBuffer, "[OSLogPrintf-%d-%d-%d] ", ukn1, ukn2, ukn3); + sint32 len = ppc_vprintf(format, tmpBuffer + prefixLen, sizeof(tmpBuffer) - prefixLen, &vargs); + WriteCafeConsole(CafeLogType::OSCONSOLE, tmpBuffer, len + prefixLen); } void OSConsoleWrite(const char* strPtr, sint32 length) @@ -562,9 +556,11 @@ namespace coreinit s_transitionToForeground = false; cafeExportRegister("coreinit", __os_snprintf, LogType::Placeholder); + + cafeExportRegister("coreinit", COSVReport, LogType::Placeholder); + cafeExportRegister("coreinit", COSWarn, LogType::Placeholder); cafeExportRegister("coreinit", OSReport, LogType::Placeholder); cafeExportRegister("coreinit", OSVReport, LogType::Placeholder); - cafeExportRegister("coreinit", COSWarn, LogType::Placeholder); cafeExportRegister("coreinit", OSLogPrintf, LogType::Placeholder); cafeExportRegister("coreinit", OSConsoleWrite, LogType::Placeholder); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.h b/src/Cafe/OS/libs/coreinit/coreinit_Misc.h index 7abba92f..36f6b06a 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.h @@ -26,5 +26,19 @@ namespace coreinit uint32 OSDriver_Register(uint32 moduleHandle, sint32 priority, OSDriverInterface* driverCallbacks, sint32 driverId, uint32be* outUkn1, uint32be* outUkn2, uint32be* outUkn3); uint32 OSDriver_Deregister(uint32 moduleHandle, sint32 driverId); + enum class COSReportModule + { + coreinit = 0, + }; + + enum class COSReportLevel + { + Error = 0, + Warn = 1, + Info = 2 + }; + + sint32 ppc_vprintf(const char* formatStr, char* strOut, sint32 maxLength, ppc_va_list* vargs); + void miscInit(); }; \ No newline at end of file diff --git a/src/Common/MemPtr.h b/src/Common/MemPtr.h index 5fb73479..142da7e4 100644 --- a/src/Common/MemPtr.h +++ b/src/Common/MemPtr.h @@ -92,19 +92,6 @@ public: template explicit operator MEMPTR() const { return MEMPTR(this->m_value); } - //bool operator==(const MEMPTR& v) const { return m_value == v.m_value; } - //bool operator==(const T* rhs) const { return (T*)(m_value == 0 ? nullptr : memory_base + (uint32)m_value) == rhs; } -> ambigious (implicit cast to T* allows for T* == T*) - //bool operator==(std::nullptr_t rhs) const { return m_value == 0; } - - //bool operator!=(const MEMPTR& v) const { return !(*this == v); } - //bool operator!=(const void* rhs) const { return !(*this == rhs); } - //bool operator!=(int rhs) const { return !(*this == rhs); } - - //bool operator==(const void* rhs) const { return (void*)(m_value == 0 ? nullptr : memory_base + (uint32)m_value) == rhs; } - - //explicit bool operator==(int rhs) const { return *this == (const void*)(size_t)rhs; } - - MEMPTR operator+(const MEMPTR& ptr) { return MEMPTR(this->GetMPTR() + ptr.GetMPTR()); } MEMPTR operator-(const MEMPTR& ptr) { return MEMPTR(this->GetMPTR() - ptr.GetMPTR()); } @@ -120,6 +107,12 @@ public: return MEMPTR(this->GetMPTR() - v * 4); } + MEMPTR& operator+=(sint32 v) + { + m_value += v * sizeof(T); + return *this; + } + template typename std::enable_if::value, Q>::type& operator*() const { return *GetPtr(); } diff --git a/src/Common/betype.h b/src/Common/betype.h index e684fb93..60a64b7a 100644 --- a/src/Common/betype.h +++ b/src/Common/betype.h @@ -121,6 +121,12 @@ public: return *this; } + betype& operator+=(const T& v) requires std::integral + { + m_value = SwapEndian(T(value() + v)); + return *this; + } + betype& operator-=(const betype& v) { m_value = SwapEndian(T(value() - v.value())); @@ -188,17 +194,36 @@ public: return from_bevalue(T(~m_value)); } + // pre-increment betype& operator++() requires std::integral { m_value = SwapEndian(T(value() + 1)); return *this; } + // post-increment + betype operator++(int) requires std::integral + { + betype tmp(*this); + m_value = SwapEndian(T(value() + 1)); + return tmp; + } + + // pre-decrement betype& operator--() requires std::integral { m_value = SwapEndian(T(value() - 1)); return *this; } + + // post-decrement + betype operator--(int) requires std::integral + { + betype tmp(*this); + m_value = SwapEndian(T(value() - 1)); + return tmp; + } + private: //T m_value{}; // before 1.26.2 T m_value; From f52970c822b7f671f6f9a80e828bf53152feb783 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 13 Aug 2024 04:47:43 +0200 Subject: [PATCH 002/137] Vulkan: Allow RGBA16F texture format with SRGB bit --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 09515993..81b0b0f1 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2212,6 +2212,7 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD formatInfoOut->decoder = TextureDecoder_R32_G32_B32_A32_UINT::getInstance(); break; case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT: + case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT | Latte::E_GX2SURFFMT::FMT_BIT_SRGB: // Seen in Sonic Transformed level Starry Speedway. SRGB should just be ignored for native float formats? formatInfoOut->vkImageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; formatInfoOut->decoder = TextureDecoder_R16_G16_B16_A16_FLOAT::getInstance(); break; From e551f8f5245f9e94f677094d56e29b11870cd3f4 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 13 Aug 2024 05:57:51 +0200 Subject: [PATCH 003/137] Fix clang compile error --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 81b0b0f1..fb54a803 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2200,6 +2200,8 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD else { formatInfoOut->vkImageAspect = VK_IMAGE_ASPECT_COLOR_BIT; + if(format == (Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT | Latte::E_GX2SURFFMT::FMT_BIT_SRGB)) // Seen in Sonic Transformed level Starry Speedway. SRGB should just be ignored for native float formats? + format = Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT; switch (format) { // RGBA formats @@ -2212,7 +2214,6 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD formatInfoOut->decoder = TextureDecoder_R32_G32_B32_A32_UINT::getInstance(); break; case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT: - case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT | Latte::E_GX2SURFFMT::FMT_BIT_SRGB: // Seen in Sonic Transformed level Starry Speedway. SRGB should just be ignored for native float formats? formatInfoOut->vkImageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; formatInfoOut->decoder = TextureDecoder_R16_G16_B16_A16_FLOAT::getInstance(); break; From a6d8c0fb9f139817b90d82775e36a4f3d1a4ce76 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:48:13 +0200 Subject: [PATCH 004/137] CI: Fix macOS build (#1291) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 015ef367..9fb775e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -225,7 +225,7 @@ jobs: run: | brew update brew install llvm@15 ninja nasm automake libtool - brew install cmake python3 ninja + brew install cmake ninja - name: "Build and install molten-vk" run: | From c49296acdc4acf3998249d8f67e8cbc984b9e276 Mon Sep 17 00:00:00 2001 From: "Skyth (Asilkan)" <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:53:04 +0300 Subject: [PATCH 005/137] Add support for iterating directories in graphics pack content folders. (#1288) --- src/Cafe/Filesystem/FST/fstUtil.h | 65 +++++++++++++++++++++-- src/Cafe/Filesystem/fsc.h | 2 +- src/Cafe/Filesystem/fscDeviceRedirect.cpp | 13 +++-- src/Cafe/GraphicPack/GraphicPack2.cpp | 2 +- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/Cafe/Filesystem/FST/fstUtil.h b/src/Cafe/Filesystem/FST/fstUtil.h index 01283684..a432cc95 100644 --- a/src/Cafe/Filesystem/FST/fstUtil.h +++ b/src/Cafe/Filesystem/FST/fstUtil.h @@ -3,6 +3,8 @@ #include +#include "../fsc.h" + // path parser and utility class for Wii U paths // optimized to be allocation-free for common path lengths class FSCPath @@ -119,9 +121,7 @@ public: template class FSAFileTree { -public: - -private: + private: enum NODETYPE : uint8 { @@ -133,6 +133,7 @@ private: { std::string name; std::vector subnodes; + size_t fileSize; F* custom; NODETYPE type; }; @@ -179,13 +180,54 @@ private: return newNode; } + class DirectoryIterator : public FSCVirtualFile + { + public: + DirectoryIterator(node_t* node) + : m_node(node), m_subnodeIndex(0) + { + } + + sint32 fscGetType() override + { + return FSC_TYPE_DIRECTORY; + } + + bool fscDirNext(FSCDirEntry* dirEntry) override + { + if (m_subnodeIndex >= m_node->subnodes.size()) + return false; + + const node_t* subnode = m_node->subnodes[m_subnodeIndex]; + + strncpy(dirEntry->path, subnode->name.c_str(), sizeof(dirEntry->path) - 1); + dirEntry->path[sizeof(dirEntry->path) - 1] = '\0'; + dirEntry->isDirectory = subnode->type == FSAFileTree::NODETYPE_DIRECTORY; + dirEntry->isFile = subnode->type == FSAFileTree::NODETYPE_FILE; + dirEntry->fileSize = subnode->type == FSAFileTree::NODETYPE_FILE ? subnode->fileSize : 0; + + ++m_subnodeIndex; + return true; + } + + bool fscRewindDir() override + { + m_subnodeIndex = 0; + return true; + } + + private: + node_t* m_node; + size_t m_subnodeIndex; + }; + public: FSAFileTree() { rootNode.type = NODETYPE_DIRECTORY; } - bool addFile(std::string_view path, F* custom) + bool addFile(std::string_view path, size_t fileSize, F* custom) { FSCPath p(path); if (p.GetNodeCount() == 0) @@ -196,6 +238,7 @@ public: return false; // node already exists // add file node node_t* fileNode = newNode(directoryNode, NODETYPE_FILE, p.GetNodeName(p.GetNodeCount() - 1)); + fileNode->fileSize = fileSize; fileNode->custom = custom; return true; } @@ -214,6 +257,20 @@ public: return true; } + bool getDirectory(std::string_view path, FSCVirtualFile*& dirIterator) + { + FSCPath p(path); + if (p.GetNodeCount() == 0) + return false; + node_t* node = getByNodePath(p, p.GetNodeCount(), false); + if (node == nullptr) + return false; + if (node->type != NODETYPE_DIRECTORY) + return false; + dirIterator = new DirectoryIterator(node); + return true; + } + bool removeFile(std::string_view path) { FSCPath p(path); diff --git a/src/Cafe/Filesystem/fsc.h b/src/Cafe/Filesystem/fsc.h index a3df2af2..8b8ed5ef 100644 --- a/src/Cafe/Filesystem/fsc.h +++ b/src/Cafe/Filesystem/fsc.h @@ -212,4 +212,4 @@ bool FSCDeviceHostFS_Mount(std::string_view mountPath, std::string_view hostTarg // redirect device void fscDeviceRedirect_map(); -void fscDeviceRedirect_add(std::string_view virtualSourcePath, const fs::path& targetFilePath, sint32 priority); +void fscDeviceRedirect_add(std::string_view virtualSourcePath, size_t fileSize, const fs::path& targetFilePath, sint32 priority); diff --git a/src/Cafe/Filesystem/fscDeviceRedirect.cpp b/src/Cafe/Filesystem/fscDeviceRedirect.cpp index d25bff86..9c62d37a 100644 --- a/src/Cafe/Filesystem/fscDeviceRedirect.cpp +++ b/src/Cafe/Filesystem/fscDeviceRedirect.cpp @@ -11,7 +11,7 @@ struct RedirectEntry FSAFileTree redirectTree; -void fscDeviceRedirect_add(std::string_view virtualSourcePath, const fs::path& targetFilePath, sint32 priority) +void fscDeviceRedirect_add(std::string_view virtualSourcePath, size_t fileSize, const fs::path& targetFilePath, sint32 priority) { // check if source already has a redirection RedirectEntry* existingEntry; @@ -24,7 +24,7 @@ void fscDeviceRedirect_add(std::string_view virtualSourcePath, const fs::path& t delete existingEntry; } RedirectEntry* entry = new RedirectEntry(targetFilePath, priority); - redirectTree.addFile(virtualSourcePath, entry); + redirectTree.addFile(virtualSourcePath, fileSize, entry); } class fscDeviceTypeRedirect : public fscDeviceC @@ -32,8 +32,15 @@ class fscDeviceTypeRedirect : public fscDeviceC FSCVirtualFile* fscDeviceOpenByPath(std::string_view path, FSC_ACCESS_FLAG accessFlags, void* ctx, sint32* fscStatus) override { RedirectEntry* redirectionEntry; - if (redirectTree.getFile(path, redirectionEntry)) + + if (HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_FILE) && redirectTree.getFile(path, redirectionEntry)) return FSCVirtualFile_Host::OpenFile(redirectionEntry->dstPath, accessFlags, *fscStatus); + + FSCVirtualFile* dirIterator; + + if (HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_DIR) && redirectTree.getDirectory(path, dirIterator)) + return dirIterator; + return nullptr; } diff --git a/src/Cafe/GraphicPack/GraphicPack2.cpp b/src/Cafe/GraphicPack/GraphicPack2.cpp index 27d423b9..c54c31cb 100644 --- a/src/Cafe/GraphicPack/GraphicPack2.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2.cpp @@ -830,7 +830,7 @@ void GraphicPack2::_iterateReplacedFiles(const fs::path& currentPath, bool isAOC { virtualMountPath = fs::path("vol/content/") / virtualMountPath; } - fscDeviceRedirect_add(virtualMountPath.generic_string(), it.path().generic_string(), m_fs_priority); + fscDeviceRedirect_add(virtualMountPath.generic_string(), it.file_size(), it.path().generic_string(), m_fs_priority); } } } From b0bab273e21f8f648de011688d96baf78e064526 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:16:03 +0200 Subject: [PATCH 006/137] padscore: Simulate queue behaviour for KPADRead --- src/Cafe/OS/libs/padscore/padscore.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Cafe/OS/libs/padscore/padscore.cpp b/src/Cafe/OS/libs/padscore/padscore.cpp index 47f3bc4f..8ae4730d 100644 --- a/src/Cafe/OS/libs/padscore/padscore.cpp +++ b/src/Cafe/OS/libs/padscore/padscore.cpp @@ -12,6 +12,7 @@ enum class KPAD_ERROR : sint32 { NONE = 0, + NO_SAMPLE_DATA = -1, NO_CONTROLLER = -2, NOT_INITIALIZED = -5, }; @@ -106,6 +107,9 @@ void padscoreExport_WPADProbe(PPCInterpreter_t* hCPU) } else { + if(type) + *type = 253; + osLib_returnFromFunction(hCPU, WPAD_ERR_NO_CONTROLLER); } } @@ -420,9 +424,12 @@ void padscoreExport_KPADSetConnectCallback(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, old_callback.GetMPTR()); } +uint64 g_kpadLastRead[InputManager::kMaxWPADControllers] = {0}; bool g_kpadIsInited = true; + sint32 _KPADRead(uint32 channel, KPADStatus_t* samplingBufs, uint32 length, betype* errResult) { + if (channel >= InputManager::kMaxWPADControllers) { debugBreakpoint(); @@ -446,6 +453,19 @@ sint32 _KPADRead(uint32 channel, KPADStatus_t* samplingBufs, uint32 length, bety return 0; } + // On console new input samples are only received every few ms and calling KPADRead(Ex) clears the internal queue regardless of length value + // thus calling KPADRead(Ex) again too soon on the same channel will result in no data being returned + // Games that depend on this: Affordable Space Adventures + uint64 currentTime = coreinit::OSGetTime(); + uint64 timeDif = currentTime - g_kpadLastRead[channel]; + if(length == 0 || timeDif < coreinit::EspressoTime::ConvertNsToTimerTicks(1000000)) + { + if (errResult) + *errResult = KPAD_ERROR::NO_SAMPLE_DATA; + return 0; + } + g_kpadLastRead[channel] = currentTime; + memset(samplingBufs, 0x00, sizeof(KPADStatus_t)); samplingBufs->wpadErr = WPAD_ERR_NONE; samplingBufs->data_format = controller->get_data_format(); From 2843da4479630e82d93ca0bb0c7e0c1748c86c48 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 15 Aug 2024 05:00:09 +0200 Subject: [PATCH 007/137] padscore: Invoke sampling callbacks every 5ms This fixes high input latency in games like Pokemon Rumble U which update input via the sampling callbacks --- src/Cafe/OS/libs/padscore/padscore.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Cafe/OS/libs/padscore/padscore.cpp b/src/Cafe/OS/libs/padscore/padscore.cpp index 8ae4730d..a83711fe 100644 --- a/src/Cafe/OS/libs/padscore/padscore.cpp +++ b/src/Cafe/OS/libs/padscore/padscore.cpp @@ -746,7 +746,8 @@ namespace padscore // call sampling callback for (auto i = 0; i < InputManager::kMaxWPADControllers; ++i) { - if (g_padscore.controller_data[i].sampling_callback) { + if (g_padscore.controller_data[i].sampling_callback) + { if (const auto controller = instance.get_wpad_controller(i)) { cemuLog_log(LogType::InputAPI, "Calling WPADsamplingCallback({})", i); @@ -761,7 +762,7 @@ namespace padscore { OSCreateAlarm(&g_padscore.alarm); const uint64 start_tick = coreinit::coreinit_getOSTime(); - const uint64 period_tick = coreinit::EspressoTime::GetTimerClock(); // once a second + const uint64 period_tick = coreinit::EspressoTime::GetTimerClock() / 200; // every 5ms MPTR handler = PPCInterpreter_makeCallableExportDepr(TickFunction); OSSetPeriodicAlarm(&g_padscore.alarm, start_tick, period_tick, handler); } From 294a6de779ddf9c6294dafa603ad23a404052ec3 Mon Sep 17 00:00:00 2001 From: 20943204920434 <160030054+20943204920434@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:22:41 +0200 Subject: [PATCH 008/137] Update appimage.sh to support runtime libstdc++.so.6 loading (#1292) Add checkrt plugin in order to detect the right libstdc++.so.6 version to load. --- dist/linux/appimage.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dist/linux/appimage.sh b/dist/linux/appimage.sh index 7bfc4701..e9081521 100755 --- a/dist/linux/appimage.sh +++ b/dist/linux/appimage.sh @@ -10,6 +10,8 @@ curl -sSfL https://github.com"$(curl https://github.com/probonopd/go-appimage/re chmod a+x mkappimage.AppImage curl -sSfLO "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" chmod a+x linuxdeploy-plugin-gtk.sh +curl -sSfLO "https://github.com/darealshinji/linuxdeploy-plugin-checkrt/releases/download/continuous/linuxdeploy-plugin-checkrt.sh" +chmod a+x linuxdeploy-plugin-checkrt.sh if [[ ! -e /usr/lib/x86_64-linux-gnu ]]; then sed -i 's#lib\/x86_64-linux-gnu#lib64#g' linuxdeploy-plugin-gtk.sh @@ -39,7 +41,8 @@ export NO_STRIP=1 -d "${GITHUB_WORKSPACE}"/AppDir/info.cemu.Cemu.desktop \ -i "${GITHUB_WORKSPACE}"/AppDir/info.cemu.Cemu.png \ -e "${GITHUB_WORKSPACE}"/AppDir/usr/bin/Cemu \ - --plugin gtk + --plugin gtk \ + --plugin checkrt if ! GITVERSION="$(git rev-parse --short HEAD 2>/dev/null)"; then GITVERSION=experimental From 958137a301208141b1e62f6b2f5b3ce2f04335d6 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:26:58 +0200 Subject: [PATCH 009/137] vpad: Keep second channel empty if no extra GamePad is configured --- src/Cafe/OS/libs/padscore/padscore.cpp | 1 - src/Cafe/OS/libs/vpad/vpad.cpp | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Cafe/OS/libs/padscore/padscore.cpp b/src/Cafe/OS/libs/padscore/padscore.cpp index a83711fe..0a577b97 100644 --- a/src/Cafe/OS/libs/padscore/padscore.cpp +++ b/src/Cafe/OS/libs/padscore/padscore.cpp @@ -494,7 +494,6 @@ void padscoreExport_KPADReadEx(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, samplesRead); } -bool debugUseDRC1 = true; void padscoreExport_KPADRead(PPCInterpreter_t* hCPU) { ppcDefineParamU32(channel, 0); diff --git a/src/Cafe/OS/libs/vpad/vpad.cpp b/src/Cafe/OS/libs/vpad/vpad.cpp index 94bb0ca2..ded4304d 100644 --- a/src/Cafe/OS/libs/vpad/vpad.cpp +++ b/src/Cafe/OS/libs/vpad/vpad.cpp @@ -50,7 +50,6 @@ extern bool isLaunchTypeELF; -bool debugUseDRC = true; VPADDir g_vpadGyroDirOverwrite[VPAD_MAX_CONTROLLERS] = { {{1.0f,0.0f,0.0f}, {0.0f,1.0f,0.0f}, {0.0f, 0.0f, 0.1f}}, @@ -240,19 +239,20 @@ namespace vpad status->tpProcessed2.validity = VPAD_TP_VALIDITY_INVALID_XY; const auto controller = InputManager::instance().get_vpad_controller(channel); - if (!controller || debugUseDRC == false) + if (!controller) { - // no controller + // most games expect the Wii U GamePad to be connected, so even if the user has not set it up we should still return empty samples for channel 0 + if(channel != 0) + { + if (error) + *error = VPAD_READ_ERR_NO_CONTROLLER; + if (length > 0) + status->vpadErr = -1; + return 0; + } if (error) - *error = VPAD_READ_ERR_NONE; // VPAD_READ_ERR_NO_DATA; // VPAD_READ_ERR_NO_CONTROLLER; - + *error = VPAD_READ_ERR_NONE; return 1; - //osLib_returnFromFunction(hCPU, 1); return; - } - - if (channel != 0) - { - debugBreakpoint(); } const bool vpadDelayEnabled = ActiveSettings::VPADDelayEnabled(); @@ -274,9 +274,7 @@ namespace vpad // not ready yet if (error) *error = VPAD_READ_ERR_NONE; - return 0; - //osLib_returnFromFunction(hCPU, 0); return; } else if (dif <= ESPRESSO_TIMER_CLOCK) { From 9e53c1ce2760ac6a33d52f8813d79402b5183203 Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Thu, 22 Aug 2024 05:17:01 +0000 Subject: [PATCH 010/137] Update translation files --- bin/resources/es/cemu.mo | Bin 65733 -> 68744 bytes bin/resources/ko/cemu.mo | Bin 69784 -> 70573 bytes bin/resources/ru/cemu.mo | Bin 89284 -> 90752 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/es/cemu.mo b/bin/resources/es/cemu.mo index 856049de037de70de90a2868e6da991fe25d1235..a43d4a1dc46c8db327cd91f2ae906dccb734169b 100644 GIT binary patch delta 20556 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 a-@xeozio_(w*BKsObmbcl=y2)%>MwjNrbNe delta 17814 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 diff --git a/bin/resources/ko/cemu.mo b/bin/resources/ko/cemu.mo index 63d82220c256cda72cbc9cf550ff5f36d8da6bb4..b812df970a34b41a6d2407c9ec4771adadd6a5db 100644 GIT binary patch delta 19394 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 D^s~21 diff --git a/bin/resources/ru/cemu.mo b/bin/resources/ru/cemu.mo index 619ba6783440be10ac2b2c0c77cda8523926e8da..4ff04e2bf7ba65f396cd0c2f640e8bc7df3c35cf 100644 GIT binary patch delta 22957 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 C7n9Kd From 573c98b2f80826c2bf0a9994c4e7d1f0950b4dc7 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:26:33 +0200 Subject: [PATCH 011/137] GfxPack: Workaround for invisible detail panel Fixes #1307 There is probably a better way to calculate the maximum width. But this suffices for now as a workaround --- src/gui/GraphicPacksWindow2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/GraphicPacksWindow2.cpp b/src/gui/GraphicPacksWindow2.cpp index 29f4b865..c49cbeae 100644 --- a/src/gui/GraphicPacksWindow2.cpp +++ b/src/gui/GraphicPacksWindow2.cpp @@ -458,10 +458,10 @@ void GraphicPacksWindow2::OnTreeSelectionChanged(wxTreeEvent& event) m_shown_graphic_pack = gp; - m_graphic_pack_name->Wrap(m_graphic_pack_name->GetParent()->GetClientSize().GetWidth() - 10); + m_graphic_pack_name->Wrap(m_graphic_pack_name->GetParent()->GetClientSize().GetWidth() - 20); m_graphic_pack_name->GetGrandParent()->Layout(); - m_graphic_pack_description->Wrap(m_graphic_pack_description->GetParent()->GetClientSize().GetWidth() - 10); + m_graphic_pack_description->Wrap(m_graphic_pack_description->GetParent()->GetClientSize().GetWidth() - 20); m_graphic_pack_description->GetGrandParent()->Layout(); m_right_panel->FitInside(); From dc9d99b03b38f3cf427714ad1ebb4d6d29f645fa Mon Sep 17 00:00:00 2001 From: bl <147349656+squelchiee@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:03:03 -0300 Subject: [PATCH 012/137] nn_fp: Implement GetMyComment and UpdateCommentAsync (#1173) --- src/Cafe/IOSU/legacy/iosu_fpd.cpp | 55 ++++++++++++++++++++++++++----- src/Cafe/IOSU/legacy/iosu_fpd.h | 2 ++ src/Cafe/OS/libs/nn_fp/nn_fp.cpp | 33 +++++++++++++++++++ src/Cemu/nex/nexFriends.cpp | 25 +++++++++++++- src/Cemu/nex/nexFriends.h | 7 +++- 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/Cafe/IOSU/legacy/iosu_fpd.cpp b/src/Cafe/IOSU/legacy/iosu_fpd.cpp index aca1a332..28d248ae 100644 --- a/src/Cafe/IOSU/legacy/iosu_fpd.cpp +++ b/src/Cafe/IOSU/legacy/iosu_fpd.cpp @@ -511,6 +511,8 @@ namespace iosu return CallHandler_GetBlackList(fpdClient, vecIn, numVecIn, vecOut, numVecOut); case FPD_REQUEST_ID::GetFriendListEx: return CallHandler_GetFriendListEx(fpdClient, vecIn, numVecIn, vecOut, numVecOut); + case FPD_REQUEST_ID::UpdateCommentAsync: + return CallHandler_UpdateCommentAsync(fpdClient, vecIn, numVecIn, vecOut, numVecOut); case FPD_REQUEST_ID::UpdatePreferenceAsync: return CallHandler_UpdatePreferenceAsync(fpdClient, vecIn, numVecIn, vecOut, numVecOut); case FPD_REQUEST_ID::AddFriendRequestByPlayRecordAsync: @@ -719,18 +721,23 @@ namespace iosu nnResult CallHandler_GetMyComment(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) { - static constexpr uint32 MY_COMMENT_LENGTH = 0x12; // are comments utf16? Buffer length is 0x24 if(numVecIn != 0 || numVecOut != 1) return FPResult_InvalidIPCParam; - if(vecOut->size != MY_COMMENT_LENGTH*sizeof(uint16be)) - { - cemuLog_log(LogType::Force, "GetMyComment: Unexpected output size"); - return FPResult_InvalidIPCParam; - } std::basic_string myComment; - myComment.resize(MY_COMMENT_LENGTH); - memcpy(vecOut->basePhys.GetPtr(), myComment.data(), MY_COMMENT_LENGTH*sizeof(uint16be)); - return 0; + if(g_fpd.nexFriendSession) + { + if(vecOut->size != MY_COMMENT_LENGTH * sizeof(uint16be)) + { + cemuLog_log(LogType::Force, "GetMyComment: Unexpected output size"); + return FPResult_InvalidIPCParam; + } + nexComment myNexComment; + g_fpd.nexFriendSession->getMyComment(myNexComment); + myComment = StringHelpers::FromUtf8(myNexComment.commentString); + } + myComment.insert(0, 1, '\0'); + memcpy(vecOut->basePhys.GetPtr(), myComment.c_str(), MY_COMMENT_LENGTH * sizeof(uint16be)); + return FPResult_Ok; } nnResult CallHandler_GetMyPreference(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) @@ -1143,6 +1150,36 @@ namespace iosu return FPResult_Ok; } + nnResult CallHandler_UpdateCommentAsync(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) + { + std::unique_lock _l(g_fpd.mtxFriendSession); + if (numVecIn != 1 || numVecOut != 0) + return FPResult_InvalidIPCParam; + if (!g_fpd.nexFriendSession) + return FPResult_RequestFailed; + uint32 messageLength = vecIn[0].size / sizeof(uint16be); + DeclareInputPtr(newComment, uint16be, messageLength, 0); + if (messageLength == 0 || newComment[messageLength-1] != 0) + { + cemuLog_log(LogType::Force, "UpdateCommentAsync: Message must contain at least a null-termination character"); + return FPResult_InvalidIPCParam; + } + IPCCommandBody* cmd = ServiceCallDelayCurrentResponse(); + + auto utf8_comment = StringHelpers::ToUtf8(newComment, messageLength); + nexComment temporaryComment; + temporaryComment.ukn0 = 0; + temporaryComment.commentString = utf8_comment; + temporaryComment.ukn1 = 0; + + g_fpd.nexFriendSession->updateCommentAsync(temporaryComment, [cmd](NexFriends::RpcErrorCode result) { + if (result != NexFriends::ERR_NONE) + return ServiceCallAsyncRespond(cmd, FPResult_RequestFailed); + ServiceCallAsyncRespond(cmd, FPResult_Ok); + }); + return FPResult_Ok; + } + nnResult CallHandler_UpdatePreferenceAsync(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) { std::unique_lock _l(g_fpd.mtxFriendSession); diff --git a/src/Cafe/IOSU/legacy/iosu_fpd.h b/src/Cafe/IOSU/legacy/iosu_fpd.h index 0a6f0885..b1c30765 100644 --- a/src/Cafe/IOSU/legacy/iosu_fpd.h +++ b/src/Cafe/IOSU/legacy/iosu_fpd.h @@ -212,6 +212,7 @@ namespace iosu static const int RELATIONSHIP_FRIEND = 3; static const int GAMEMODE_MAX_MESSAGE_LENGTH = 0x80; // limit includes null-terminator character, so only 0x7F actual characters can be used + static const int MY_COMMENT_LENGTH = 0x12; enum class FPD_REQUEST_ID { @@ -245,6 +246,7 @@ namespace iosu CheckSettingStatusAsync = 0x7596, GetFriendListEx = 0x75F9, GetFriendRequestListEx = 0x76C1, + UpdateCommentAsync = 0x7726, UpdatePreferenceAsync = 0x7727, RemoveFriendAsync = 0x7789, DeleteFriendFlagsAsync = 0x778A, diff --git a/src/Cafe/OS/libs/nn_fp/nn_fp.cpp b/src/Cafe/OS/libs/nn_fp/nn_fp.cpp index fc757ea9..86ca4708 100644 --- a/src/Cafe/OS/libs/nn_fp/nn_fp.cpp +++ b/src/Cafe/OS/libs/nn_fp/nn_fp.cpp @@ -464,6 +464,14 @@ namespace nn return ipcCtx->Submit(std::move(ipcCtx)); } + nnResult GetMyPlayingGame(iosu::fpd::GameKey* myPlayingGame) + { + FP_API_BASE(); + auto ipcCtx = std::make_unique(iosu::fpd::FPD_REQUEST_ID::GetMyPlayingGame); + ipcCtx->AddOutput(myPlayingGame, sizeof(iosu::fpd::GameKey)); + return ipcCtx->Submit(std::move(ipcCtx)); + } + nnResult GetMyPreference(iosu::fpd::FPDPreference* myPreference) { FP_API_BASE(); @@ -472,6 +480,14 @@ namespace nn return ipcCtx->Submit(std::move(ipcCtx)); } + nnResult GetMyComment(uint16be* myComment) + { + FP_API_BASE(); + auto ipcCtx = std::make_unique(iosu::fpd::FPD_REQUEST_ID::GetMyComment); + ipcCtx->AddOutput(myComment, iosu::fpd::MY_COMMENT_LENGTH * sizeof(uint16be)); + return ipcCtx->Submit(std::move(ipcCtx)); + } + nnResult GetMyMii(FFLData_t* fflData) { FP_API_BASE(); @@ -607,6 +623,20 @@ namespace nn return resultBuf != 0 ? 1 : 0; } + nnResult UpdateCommentAsync(uint16be* newComment, void* funcPtr, void* customParam) + { + FP_API_BASE(); + auto ipcCtx = std::make_unique(iosu::fpd::FPD_REQUEST_ID::UpdateCommentAsync); + uint32 commentLen = CafeStringHelpers::Length(newComment, iosu::fpd::MY_COMMENT_LENGTH-1); + if (commentLen >= iosu::fpd::MY_COMMENT_LENGTH-1) + { + cemuLog_log(LogType::Force, "UpdateCommentAsync: message too long"); + return FPResult_InvalidIPCParam; + } + ipcCtx->AddInput(newComment, sizeof(uint16be) * commentLen + 2); + return ipcCtx->SubmitAsync(std::move(ipcCtx), funcPtr, customParam); + } + nnResult UpdatePreferenceAsync(iosu::fpd::FPDPreference* newPreference, void* funcPtr, void* customParam) { FP_API_BASE(); @@ -763,7 +793,9 @@ namespace nn cafeExportRegisterFunc(GetMyAccountId, "nn_fp", "GetMyAccountId__Q2_2nn2fpFPc", LogType::NN_FP); cafeExportRegisterFunc(GetMyScreenName, "nn_fp", "GetMyScreenName__Q2_2nn2fpFPw", LogType::NN_FP); cafeExportRegisterFunc(GetMyMii, "nn_fp", "GetMyMii__Q2_2nn2fpFP12FFLStoreData", LogType::NN_FP); + cafeExportRegisterFunc(GetMyPlayingGame, "nn_fp", "GetMyPlayingGame__Q2_2nn2fpFPQ3_2nn2fp7GameKey", LogType::NN_FP); cafeExportRegisterFunc(GetMyPreference, "nn_fp", "GetMyPreference__Q2_2nn2fpFPQ3_2nn2fp10Preference", LogType::NN_FP); + cafeExportRegisterFunc(GetMyComment, "nn_fp", "GetMyComment__Q2_2nn2fpFPQ3_2nn2fp7Comment", LogType::NN_FP); cafeExportRegisterFunc(GetFriendAccountId, "nn_fp", "GetFriendAccountId__Q2_2nn2fpFPA17_cPCUiUi", LogType::NN_FP); cafeExportRegisterFunc(GetFriendScreenName, "nn_fp", "GetFriendScreenName__Q2_2nn2fpFPA11_wPCUiUibPUc", LogType::NN_FP); @@ -774,6 +806,7 @@ namespace nn cafeExportRegisterFunc(CheckSettingStatusAsync, "nn_fp", "CheckSettingStatusAsync__Q2_2nn2fpFPUcPFQ2_2nn6ResultPv_vPv", LogType::NN_FP); cafeExportRegisterFunc(IsPreferenceValid, "nn_fp", "IsPreferenceValid__Q2_2nn2fpFv", LogType::NN_FP); + cafeExportRegisterFunc(UpdateCommentAsync, "nn_fp", "UpdateCommentAsync__Q2_2nn2fpFPCwPFQ2_2nn6ResultPv_vPv", LogType::NN_FP); cafeExportRegisterFunc(UpdatePreferenceAsync, "nn_fp", "UpdatePreferenceAsync__Q2_2nn2fpFPCQ3_2nn2fp10PreferencePFQ2_2nn6ResultPv_vPv", LogType::NN_FP); cafeExportRegisterFunc(GetRequestBlockSettingAsync, "nn_fp", "GetRequestBlockSettingAsync__Q2_2nn2fpFPUcPCUiUiPFQ2_2nn6ResultPv_vPv", LogType::NN_FP); diff --git a/src/Cemu/nex/nexFriends.cpp b/src/Cemu/nex/nexFriends.cpp index 927418ca..36ba4a53 100644 --- a/src/Cemu/nex/nexFriends.cpp +++ b/src/Cemu/nex/nexFriends.cpp @@ -277,7 +277,8 @@ void NexFriends::handleResponse_getAllInformation(nexServiceResponse_t* response } NexFriends* session = (NexFriends*)nexFriends; session->myPreference = nexPrincipalPreference(&response->data); - nexComment comment(&response->data); + auto comment = nexComment(&response->data); + session->myComment = comment; if (response->data.hasReadOutOfBounds()) return; // acquire lock on lists @@ -391,6 +392,28 @@ void NexFriends::getMyPreference(nexPrincipalPreference& preference) preference = myPreference; } +bool NexFriends::updateCommentAsync(nexComment newComment, std::function cb) +{ + uint8 tempNexBufferArray[1024]; + nexPacketBuffer packetBuffer(tempNexBufferArray, sizeof(tempNexBufferArray), true); + newComment.writeData(&packetBuffer); + nexCon->callMethod( + NEX_PROTOCOL_FRIENDS_WIIU, 15, &packetBuffer, [this, cb, newComment](nexServiceResponse_t* response) -> void { + if (!response->isSuccessful) + return cb(NexFriends::ERR_RPC_FAILED); + this->myComment = newComment; + return cb(NexFriends::ERR_NONE); + }, + true); + // TEST + return true; +} + +void NexFriends::getMyComment(nexComment& comment) +{ + comment = myComment; +} + bool NexFriends::addProvisionalFriendByPidGuessed(uint32 principalId) { uint8 tempNexBufferArray[512]; diff --git a/src/Cemu/nex/nexFriends.h b/src/Cemu/nex/nexFriends.h index 1077b0d5..05cc433f 100644 --- a/src/Cemu/nex/nexFriends.h +++ b/src/Cemu/nex/nexFriends.h @@ -297,7 +297,9 @@ public: void writeData(nexPacketBuffer* pb) const override { - cemu_assert_unimplemented(); + pb->writeU8(ukn0); + pb->writeString(commentString.c_str()); + pb->writeU64(ukn1); } void readData(nexPacketBuffer* pb) override @@ -554,6 +556,7 @@ public: bool getFriendRequestByMessageId(nexFriendRequest& friendRequestData, bool* isIncoming, uint64 messageId); bool isOnline(); void getMyPreference(nexPrincipalPreference& preference); + void getMyComment(nexComment& comment); // asynchronous API (data has to be requested) bool addProvisionalFriend(char* name, std::function cb); @@ -565,6 +568,7 @@ public: void acceptFriendRequest(uint64 messageId, std::function cb); void deleteFriendRequest(uint64 messageId, std::function cb); // rejecting incoming friend request (differs from blocking friend requests) bool updatePreferencesAsync(const nexPrincipalPreference newPreferences, std::function cb); + bool updateCommentAsync(const nexComment newComment, std::function cb); void updateMyPresence(nexPresenceV2& myPresence); void setNotificationHandler(void(*notificationHandler)(NOTIFICATION_TYPE notificationType, uint32 pid)); @@ -619,6 +623,7 @@ private: // local friend state nexPresenceV2 myPresence; nexPrincipalPreference myPreference; + nexComment myComment; std::recursive_mutex mtx_lists; std::vector list_friends; From d7f39aab054a715e7b0481407298cbd654212fb9 Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Mon, 26 Aug 2024 09:16:11 +0000 Subject: [PATCH 013/137] Update translation files --- bin/resources/hu/cemu.mo | Bin 71267 -> 72404 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/hu/cemu.mo b/bin/resources/hu/cemu.mo index dd3d2fbc74157e457fac7fefe3356bdbfbbe9e11..51b00d083cc320a55615d50774d60c74f71108d5 100644 GIT binary patch delta 22802 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 delta 21762 zcma*ucYIXE!vFEJNob*k-eE&8Noaxy3WnZ`0Yv(eY?39(Zrt4j25p6g+-`}?l$=kVgl(E$QZ0w zu{pkH@-Jd_(s6xk>tIvbw^E4c!8BBb8CV(vNDr+dEQ9xopF{qADM$O#q zSOf1u4dfA2ho42&zZTVzi1959sp36En&Bs?2Ct$XEZfg+s48j?)I~MW5>>7{*2aD& zJsH&z52|B@7>9Gr{rSelSepE0{h0qUM4l%@4X#Cva1*M6J*W;IL#^HS#;d3fRPS$3 zbpzB?w?oZbZ<9Y9HR5#C0KBLH+<_|paDV2%43U*&s7LFtEN(z`=q*$Sc4I9(iZ$^( zmc=py>;@B1>H4U8nwfMGsso)+&)m@jw5gn#$vre z_DnRwc+zcA6?a8F*ApA!Xw>rs*aPp!J8>84wd_6EZf_#8Bq1x0h#HuQBQb;;;bBw* z-{KrRhdpuH5W6FrQRVic@;^sjamzW>vQp8FH{uJZ8TuME1HWTAyozyp|H}=tE2@MQ zxRHpO!W8U;Ls7eUHtN9#P`f^i74aohM>nJPz;@$4)XW?TOu(b4y>uG23AL`Ow>E}U zVIv|c*a5XUdZDJwh260bJL3kE{{u36mg5$?!r}M{>1>>eJ<}}f23(68$hSBOFQPg= zfOUQh(?&A?ONpE%LsNG@rR0OCku1e(xCym(`%qK;KB}XipqAoSlb|0`C%+-; z$7eKlLBF}b#^k>;n)z3S`^iwlN3b#;N9~0lQTPAA0T_ELzmzxx+hGB!gDbHIZbq%~ zWz=(J$Jk4ofGStd*cMf;SBS_GA|p^0o<=shRrfZIRm{TLxE{4syHQK?DYnM*7_E4$ zy;yp!(~_pH>2K$!|24HQ5~r< z-mbVMt|dJKHKG<1EUO!KLe+OWs=?`~C7g@e%nu>;gsex1s3$L=mS8=q;Okfu-!dLS zmHPoz@z1D=|G<`5kzwmK?Tp%V-B9)QLzN$nnz>s|el8~H{VyQWo*Vb0dj2|Q;?J0i z<0jencVi9Gmr-v^!erY-bdv6X+B<330@G0)nTK_83F>uy6;;ncY(V?gH$*h0u`c^Y zO;iI($ctg!gzEXj*Z^NZ&CpIv$9>or+fK1JX(4K6wxP;>fZ8KRQ4Rlqn#mX*4&6f} zo`?tMqi$S6t^F0$64cJHJJ1%j8GEBTJOOKAKB}Srp!U*2)JPx0Hnnqefg4H6ux= z_PS#|^k*^us%Rk@TC2^dk?lfF`6sAd{Uho;_!DEXTsEI&SP@lGH`MERD>lJdsF_=d zNw^g?pzpCER%g1KW5*B?P4!spia~6Q>rrd{0Vd*k)QGB1wXK8NY)(`M+nN0CsPcVG zIt#TFUexBEXY$`Qh7J(X$d6zgeudhtr%;>5kz>zD4U8w<64hWwRQXhs-xte~9%0hs zP&1Z^WiWs&zcmvzkT320ko7GQ?e=rn6!p$&3Y%dw9DsVjhnliN)X1MQuEh$Zx1u_{ z7uDc#lYa`eq!&>gFP&%q7F5SAdjE$IDa(y`uU%0!j7}|Tx3@!8IKbQ=fia{fqGrH_ zYcUhm(ds^Xv(-cGjbv200azY~Vg($FmGu5+5vhoUs41L{m2n|z%9o*b?`qW2yonm= zN2pEuJ?gous0PaAbC_WboQ6*#Z?aYWb`B?;j;Rj*W`rS)tWv-}v1+0!?2e@{4K*{j zp*k=HH6uZDe*tR5VN|)tQ5|>|RquLK`Aw(}9zxB;G2<5j=3npU*JP-I%cz-%4cZ@x zRZ#D}6IF2<>h+t1DxZ$3Fc-B~Zby|{f|c=E)Y}z7mEVtgyFNla_j{1}*9fkVp$AJ9 z*gda;Do_Wt1P!qUw!(4P1J#iyQ011RHt}<)hBp{@n)^pk9XN*CJ14Oko(d6B4=-bN ztX^muLRH)YmERk?<87!JT8d3^rAhBYHSh_lffK0r{TtMIkjO?*hkK!xrXQ+YXc!Sy zcnhZBWV{(4M6KN+R0mF@dVB>dVb$q&IuW%uI-$zvfZ{19ddMkDBuHsHOP>HG-HK_SxMOH8W#Sn`#1T_>6xro^ zq6RVu%jo@|L_{OXLakvTs=_&_4m^ZvXbI}ASb^&33s@6(p+@>Ss{A*o@;{(PehGDy zSD0xZ#jR20`eAjw|3is5F&(?%Y}6EOGww$%beP31sTLt{}hl8tK6i+ZjA zYvIGlzP4UO?IGt~c72^td!+we%)ctiBcn0SL^ZGsHMQ$78FykGyogP(BGaZl(H`4i z8me3fJL6is9=}GNANB9H|4ujz8fpSTO^d39EBi1E75nJF4 zR7Y2!MzF)APorkC!Yq5uYoG?w2Gw9U)Seh?@-t9N5)2X1>m!x12(|W4qB^n~HAAnX zHq$QD%zS3lfo+s2M7CpIt#MR0T~j2D_j-*bOzsJxzKrs@^oz%uYjfWEQ#{yw;eg_y4E+IYzlr zeU80*-$W>_U`o~|7X3%A4%BjL3@|yV8=WW?dG7>j?RcDwZmqk@{)6W>O4IA*au zGmVh_YK=!;1nW!GUPyn`?%-6^-kFINZ~->Q$FUl|^C>cK0hh7z8zEGEUOjuIGio<|L!+A{kuGKpA+^mx1p3$YkqLv?ie z^5|X(S$7iAkHh__sd@r?;tJH1er>#r8hOIgb_KOi1PMzRL=z#FI@zl}9<2R6e`Q74{bh261=sB+a&OHc=6Fwxv^ zirQOkP%~VF(ck}hL@GNh>uJ=Kzx<3{U^}W~AEG*R(s;pm)mZUa`?-2pmvYId&D9^Z z>)oim zHB-w_uh|A{f_pIfJZfNNpXdG8$Z9`t_prIK9cpU3qSmS}sw20d-ro$=K{F3E(nrvV zub>+I0JT?6p`JU7D*v0wcdWG2Wmksm2CA5hrlQX1G`0=BU_a2=*c~Ru0T!g z7L$G#)sYjZy>t=vBUNFwebC&18rT?AhcawKRt^#E0Y7R)cVi=5jID43Y6iYQRq!L0 z#mlG;#J*@hUkR08A6sEFR7ZxPp36qfXb?5PdC`2stcWUN6wuotykPoid^%o=;d zO;Kyu1KZ#PR6`G7DU6^-_J(mAs-AaIGjj$tkUvpxPx+UaQQEhf5piP|jKyWBbKn`& zgRh}xW+#@%W7rf=p+-`Et^K{w2A6jp(`5{zAC$TMF#dg^CW&5*xGWH_95Vdq4pz8Yx)!?tj*jMa5R@vC%72f|A z6zD<52+YDV_y+31cTiKlA1mV-)EfSQ<*@3j_S)A+EnRD4FRVm*JnFd|Ou(5~0q3KZ z_OVx)e^s=RjCkCJT7rG3DLja(=rn4|e?wK!AY$KdjjE_OI?;t%^I6y!SEBaB9+N(V z8qh`TiB&?c*$<4w(PT`=c6bm~(AvOyLPJ!Aci|aafrYqWqy5kDahvQBj=~)BGfnyf zWn-uFjPYr7t`LOY3;sgU(05l!iz#&U1k|4>*H+mk;NyWv{YjGRWzRGBUI zrksd+z6iC(bFd07MK$~)R>!@_;H@w4W?Z^e|GFOHpG=61BO~Q4yMei=^I#R$!gsMH zet~tc?AvyS8e=x;iKyNGE~aDoZT63p59^VB6g6Y7VqM&f>hKYChKQUXl8CXqdupf| zHpih@4GS<6AHZFB9yQWU+s*ers)5U>rHm=Ir??)*l1{`L*bG&!8)|8KVMsk0LPS$L z);Jy2z@w-m^##jxCBSwCDcg!@3E&m1J&SEY>2Z_9eWlZ z!B;RF`|h>NuMZJXPc|42VO7#+P#wEs^6TxhH)CgvBY!BC#WYlh$74?{!j8BVJK+T! zj4j@?&xs%oBE1tefKZkF_9jZl>&aM*U2#9^y)Jve{!8SosP}d*mcfUx94^HQ_zXI6 zJ?eEkgzCsQn22=_+DqFFbtF$i1{kvD5YY&pLv51nsNMP@#^P!0jAt>3DwYz*a!!q?&p~M0o3ll7uCV1@J3vZU*Zp_^7}tFC*a4de*zgN$k6L{ z5mlhVQG4pDV+`qrs9m3kZ(?)QraOT(@EmGwOMPPJw=#Cds^s@Vy-lOBHU>~jF#i+g zUllANLnB*@)o=@{!b7N~`PrCo%*+HvX96GQ{ve!=$1w|U{gl%b-^PUwru{Sf?OFY~ zy;;{|E$+V`BBCijZ9I?az^~XC%OAHNOhJ{q&ZN6xb<(M*y)qir(G1kg+>JUP7NAD@ zC~80}P&2s`wM3!4M6@eE!}0hX*2O+w*qdiEs$d4H1BKWT@5DOz3RcE_SQ9@*HT)Cm zb^Qa?V9PJxe9M$m(Hk^h5P@2&&>SI1>G+UHmSpqJ5~5A409=H>mPgPxT_+eAEy}CJ~)%OVNqTus^lx2g>m>Ys=^JZ za&O~AJb+s3h zMtY-Omkd+|cVHR(531Zls2O`2o8exQK98!e?sxXbX)EkUdK7lXr@o`xIv5U<(GV}7 z9!NNCf1xzOG}2RX1ippZqzT{Kd!rsIorF#CderMU9?RfVqaRgo5thOSO#Xu*BHG1E zP$PU9HHEuT9r_G4g@2$%UgeBEE@_4AA#$z0JUkG{$TH&PS}O?VAN}TA8PY0 zLDd^NLc~erjJZ+atUXl&u`Bt7sHu7dufy*!1snWm|7Hv}&c+VpZ^1Em4m)E1bM{+t z2R0}DJgVG#NC!gJRU+zPjq~=}4aHWZgV++68Q;Ygq|aj|tbM^Q*8(+?5vaYAg}Ofn z_4Yi9m2s`PztyBa$Ew=4XNl;ZEhA z9NuH{=b=XYBv!|lPz}F>I{Efs1^gDH|NZaJCL`__yTK}0jRK8P=R;?V!CO!x8HK8F zJnDSNz;@`tczhhy(dY3t2YUjwlr?|l7{?x{&0Xd<=3i6Y{5LA4-~enxdin4CgyKHb z6gRnSpO9m5FzGu`9oU0a@O{*hoJ749m+&wq{$U?XmoSNR;h&Z@8K1=)XfN(B=3l>h z-LLRpH{cVf*W(Jlfj3^YJ8%j6l1{c9(FTHe9qHxR5f7u5u&l!o{VyB4;4B*W0M(&U zv3B{paWv@-s7+cS6z7OeRXx-QI$$F9K~xjNR(@=ZmVN{3KVG3?T{rsLoZN75l9MR3$0-5oUbv+Snj>l0QDMmeT3cYB>JEGt7 zKGddKk2o1_5yM)`ZbVYk<_Mn#R7^=RXusznUWOsNVYAJ$PL+}3r zMetcv#c$x9_!iEKVH5KI9Oy%OcNItU?Wk1MZm27ECw~y?`MKB)pT_n08ET2Kz$0`b3yKTQ7T1YPxMwA1&Qvi2SOZ_(wiskfuK|Ag|i{zFVj?b@Ho+(3cG8jiX4;&0^7 z!H%XP-ae}=Wk#E{W@#Dm4^8En#0QdhlJqK*cPn`v2qVaEhq^-db8`TRgLe8~e||@5 z8{t9945!Sq#D|y%d7b~hyyPVlniCq(sr$L3)9>1A1NXL*v4(I5@pbn7kp6+wnoQ7j z7dLb*H!A+1{@{A$nhN=$w0D z#+iqAmz2Z`regiLoTZH3+mh=)L>}jv3xs-vEu<%-2Uk*dKXF~(66z9W6LymSxay(a z0=Al)jiwK>^)>S;@6Tb(*qE@lnDcbMvkci54V2q@j}Q9U?kz4{~ol!AHCj zVG}{uE0ooL$5C<(B0iBsENgqweKAP@S?LVdzN;zC@%yH~+vL z1bwmSs!Lu|(jS{jyW)GKbu}`1cIux$U*P^B?mcA6>CRpgUqZYa;aih_SL=Thi7q@) z5nsZ(n69VHwSx2;AroKMNaa=t0so%`-QUHovVSB=Kz5lvCK!=IHi+2)krNCD1{Twa9_dID2`A=ZU)!sZ)56jc|dUNj!(!Gek zO?b}~zQvRsN#4E07hxz!iRUUG*K%(b;VS8DtWBOiqjh~j zxSjMjCOw7pX9Rr-HKSv-2&0JiBz#KHbw=wymdH^;5)bd?;mX9zP&keBm&A3|ApRyn z*EfXb-0xr-Qr=VCTT8f?_)CP@#9NvB0lcC@T+f+0+K_jGbYrc5gv?mNZDig|7)QKH zw7C7p59H}eCN!bp8POCQmH1QKTV&GRcuvG8 z@?y*bB&`n!SGhl!u-iPl4eL;D59MAX-h{APg}M0R{`*=@e!SX1bA_zONPIw{)8u60 zJ%mZb7m#02QtTS%{PVE&6J>{S?`iVBCVmJ%CcTsR>!#dS#JiJM4I5)A@*TwOOY=`6 z#}v-Su(|n~@(HgK`jFqBqCOl-x{G;Sq>$EMNJFUWSIRs|_=%wFMQqGH{w`wGBD_HO zlQ4jg5Mnwi5`G~Ou*+NJiQh?Z5p=Dgf}w;{CViOrQbNg9MC4JEeirYh+*HCq!WV?^ z2qo7?M5a(?26;7!hZ^w5K{AR7-LEOwfON@qmiz;xUnJvC!g@jr^2QSS5HGssxe3JQ zQT8pu9>QgF?+(gUB#b8gE#XT-yxz!8ra%p5p@4WMg>?N+{ADW0!*i(XM&jp*zfb&b zlc)S6`h)8fd416pO);yadlUZ>Z^Z9#6Jaj#`v{wLBJ3lhkWg~fCNhllMnXCbUXLdU zmFeIt)Rj-@O1Rmi%VHe&k5fK{_(R-FBcv1Wg#8GYxOYG4nv^R=c%Jkl+BEtN$VOeC zVJkdf(#m^)`^WKK@+M$q(z-suG1v&}bMF}8PSU>;7Mn5)h#xg^qS1e}l2-{E3EKbG zGD005{t)k>@LJ-rgt^4&sCAN{Ydj%B{xrh1*Cfi_K=|vw@>QlO!Si4FFPMk-$IyNP zH+K`3lj)|C>87w7yO4gB{81)P&$T7~7@;8{kNdxqHy;zRkh~oPUGL$aK;z{^4;cN14)BIN^vl5B(#2+W@B0iivUC&}Y*5Dy5Ce1C& z$scn_S0#Rg_&UO1;<}EK|Cb7J?IQFr@!QE?PrNCfA-K6WgLI|lL_RZxZnnbfdasBX zJ|Nt;PqXm7eZ1k(D-y!r_G#74=XZO&o}hEcaA)f90ao)~e`mP!eEu2#l{qro?{;OF z%<%FRO~RG>t`6_-+p+YR4$2Mx)i=44I~eqMvz&pT%O7-Sgj@9M)TBe_>+Fa6yL0oc z<^#Oxes`YR8&ovEAQ*jgYQG!88~e>~HMDOpyX?@R&Yr{4(yZo0`VNmWH0sFIe$gjx z?w?&{_${M)4|lfA^!c69Vy(i*`Y(=ESF#5TD(6ejaYt*9ygb0?h;Ql34|;rFS8l7Y zb5Q5u&h&yn(3j`T%T0IYyMo!yOg|}?GvDtn^!N$_&R*`k0;k8zNOE&sdeWKg3OJ{@ z-Ck#=ufUt(bP+N=xw-BPilszG4*D`St{}tXiyR*If+OHDV^8RRVdW1`*{T9A?R8IK4QC-Iue;Z`+fed&YA7*j34RpXEDc`zpn8!+dn)Z zEpp$4V~$cxYP2Ny+d{^XB?o88npTcjae$vm}u@go`9-y4d~7X%ofRyfF; z<-FD7agJh!*-m6@HUrLFPYzqeRp<`aUr?t(2Ah$kc3ElBO~ZPaU8)%lm$~QOSZWU^ z+*3U=_nrfe$n1N^#YS4pNr($SUJxHL6B%%3_&tTJScZ@K*;6cHe!kDoR-<>L3vzQ@ zUZ;C{(CrOqQ+wU%?m)oB+phiL3e51PXZw9#y#ax27KrWT%gguJuRw}3_1Xg0+;OId zTwXOc#T^}WO1v}vmXf{ncWYgpqX$juGiKzZd{4eR*W-0hif%mmF)7`Zo=wu{4f=h# z@$m!f7f)x2x~(Y-1WWeH-$TrE&2R>MxrJ^Dm1Dl$+L|fhmh*-*$Z%)63UWEySi$J~ z&25j@m9Eix!tX4qUfX^#nd1ykW~STEiDbWek^FgKM?$VIU2jD61R;W{hVpe%v@I>JG#$Y zS?(0)fFR4}4f5*n2613ShoNm2NGf^%*@E`DLJ3Z?-h+FE0x9u4$V3HL1aDUE3^prk z%wf$g&UOW(@ABVA1uw5hbHrA*j|gobeog*&h8WlQR?heVna*5yX3*)*%MZ>-iVjC3 zHcwCUG^3+zAG-k^ssT@iR@QBw#df)r$o_?II3l+#a+R&>%y;|qTwbl5-Y`ZU+4$H~ zv61he`ZcBntKrwn$SG!82)NyO0lkhilILL?c(YRC!w){yC{8b9*twuahJ9k#a~pj@ zQ{tm9wR?KLeTbW_#A$bD{C}V0W@gl3(;`!*)W4qY5&rUNUwFrg8^cweX_{bnP_NZ}GZ=?i_y3hmQ&;tXy#O-#_ckGmBSD%}vhs6%`e)$Shv%FJ2w+ zus?EKxp^+K0{MO)r%0YpagQ_8om;#m*BuN$xw6fqzbhHi-2PhlJEJ+jMa2=n#})0$ zKiL6l$^F0bk^@B^Zw@=mqY}jtl?m@!`A7KJ3*F1~GanN*SKUyy`5<>u27L? zfAbv?esEQ@Qqe(&+a7NaE?(6>Txs>1*3JL_KOJ6QJ-6i3A^O>nob4`Z72fvZXVFiG zMQd)q=F{Qonx7mAExqnMXHN0PAg6NC&PA=lKdtSaVQlE)6AjMG)Xw+LSuIRya@5GtX@++a#1 zW!K<@Gq-Su>n@TIQU8^`=MU(53`Oc>d-Y;p8`mg?GPMC6xO2NT>2p5gVpR zPZzJI77d17Vs=tNkm0!g&ySCPdN@TVWRbIY^VDl+$(_R@q*$XhlhN(xb>*`)tW;;@ zhc{O_9D^c@x87MgwolfK@JGdsZ=wSG&B=4+=QHKhsWoCWZa;fA$ZOl*GsR_prSxL- z#hZhL`uNjlN;qLhLFv8)L3$fLyQ5y@)Q&PSrS0k>7j~_UX+Ffw7i`JIarC)!bj~td z7YA6dcw;Uh7+i8s8AtVMJ*=UeCX75l!QzcmrtrQzwP$1`cJD5Sqkm-YzCMn~r2Tyy zj?~EW2Ue7dY&e`5>*yP)e>5q^F*H2j*xqp4PZz}T(UKfK_~~3nN@VP3E=Ty{=UpN< z9p^t67#6n1f0u+d8V~7ww-pXJC;VzkV+!vg+H}j!41j^A5-GNamTb zv61(FoZ;a4zUR+`QZuGH*{RV*FIl>OSEHqi?^DDGqlaH|A-^d(uKaX!q$p`BDS!-Z*$WlKV29T6Uq8HE3RB$pI=+v zN{cvuf7Ia^8QFCChZuJ4RNg_aJ{dCgS|#gComXb(B6G+^Hw?qc^JTe$Q}yqN_U>O)Xv>Okx80LCke$yUF4GVq0*IX)7}c_Ic)HQoQs$$Cn#^>D3CM zV2ZO(p2x*Y>MqiYky+r)N%s^-g3-21CXXLhUj`diCwESucy$(^ZU1Wi|NpCLuzF`# zk>t!CD0{ z=RdzEwC)Zv>nYcsJGAG@FeeUgy(`!wa^SCIN36?}5pHs|ca4_Ab5s83V=A0^bzu17 z)z=)(#j6|>qF+Y;93jyYhz}th7UrvZiaR&3b5V4AX0uN09(RtDU9>TW!^u7*rn*>; zBB%al8vVWF8^D{v4zc%WfE6w|wK+Mj-KarlNSl-`n#{q{$G>w?fX7^HasCby#S5^4E%J^p-UQdrDt!p_p hFPU81F>J}U+K!YO_G$gkY*+zbwrf-WI*$2{{|9Kk&?*1` From 1234e2c11808a97c76381f147e564d74f6ffbbd1 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Mon, 26 Aug 2024 11:43:38 +0200 Subject: [PATCH 014/137] Preparations for 2.1 (#1306) --- .github/getversion.cpp | 9 -- .github/workflows/build.yml | 66 ++++--------- .github/workflows/build_check.yml | 3 - .../workflows/deploy_experimental_release.yml | 93 ++++++++++++++++--- .../workflows/determine_release_version.yml | 74 +++++++++++++++ CMakeLists.txt | 27 +++--- src/Cafe/HW/Latte/Core/LatteShader.cpp | 2 +- src/Cafe/HW/Latte/Renderer/RendererShader.cpp | 6 +- .../Latte/Renderer/Vulkan/VulkanRenderer.cpp | 4 +- .../OS/libs/coreinit/coreinit_Spinlock.cpp | 2 +- src/Common/version.h | 29 ++---- src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 3 +- src/config/LaunchSettings.cpp | 6 +- src/gui/CemuUpdateWindow.cpp | 4 +- src/gui/DownloadGraphicPacksWindow.cpp | 2 +- src/gui/GeneralSettings2.cpp | 56 ++++++++--- src/gui/GeneralSettings2.h | 2 +- src/resource/cemu.rc | 6 +- 19 files changed, 261 insertions(+), 135 deletions(-) delete mode 100644 .github/getversion.cpp create mode 100644 .github/workflows/determine_release_version.yml diff --git a/.github/getversion.cpp b/.github/getversion.cpp deleted file mode 100644 index 469a796e..00000000 --- a/.github/getversion.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include -#include "./../src/Common/version.h" - -// output current Cemu version for CI workflow. Do not modify -int main() -{ - printf("%d.%d", EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR); - return 0; -} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9fb775e2..dd28ceb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,10 @@ name: Build Cemu on: workflow_call: inputs: - deploymode: + next_version_major: required: false type: string - experimentalversion: + next_version_minor: required: false type: string @@ -24,25 +24,17 @@ jobs: submodules: "recursive" fetch-depth: 0 - - name: Setup release mode parameters (for deploy) - if: ${{ inputs.deploymode == 'release' }} + - name: Setup release mode parameters run: | echo "BUILD_MODE=release" >> $GITHUB_ENV echo "BUILD_FLAGS=" >> $GITHUB_ENV echo "Build mode is release" - - name: Setup debug mode parameters (for continous build) - if: ${{ inputs.deploymode != 'release' }} + - name: Setup build flags for version + if: ${{ inputs.next_version_major != '' }} run: | - echo "BUILD_MODE=debug" >> $GITHUB_ENV - echo "BUILD_FLAGS=" >> $GITHUB_ENV - echo "Build mode is debug" - - - name: Setup version for experimental - if: ${{ inputs.experimentalversion != '' }} - run: | - echo "[INFO] Experimental version ${{ inputs.experimentalversion }}" - echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEXPERIMENTAL_VERSION=${{ inputs.experimentalversion }}" >> $GITHUB_ENV + echo "[INFO] Version ${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}" + echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEMULATOR_VERSION_MAJOR=${{ inputs.next_version_major }} -DEMULATOR_VERSION_MINOR=${{ inputs.next_version_minor }}" >> $GITHUB_ENV - name: "Install system dependencies" run: | @@ -81,12 +73,10 @@ jobs: cmake --build build - name: Prepare artifact - if: ${{ inputs.deploymode == 'release' }} run: mv bin/Cemu_release bin/Cemu - name: Upload artifact uses: actions/upload-artifact@v4 - if: ${{ inputs.deploymode == 'release' }} with: name: cemu-bin-linux-x64 path: ./bin/Cemu @@ -128,24 +118,17 @@ jobs: with: submodules: "recursive" - - name: Setup release mode parameters (for deploy) - if: ${{ inputs.deploymode == 'release' }} + - name: Setup release mode parameters run: | echo "BUILD_MODE=release" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append echo "BUILD_FLAGS=" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append echo "Build mode is release" - - - name: Setup debug mode parameters (for continous build) - if: ${{ inputs.deploymode != 'release' }} + + - name: Setup build flags for version + if: ${{ inputs.next_version_major != '' }} run: | - echo "BUILD_MODE=debug" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - echo "BUILD_FLAGS=" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - echo "Build mode is debug" - - name: Setup version for experimental - if: ${{ inputs.experimentalversion != '' }} - run: | - echo "[INFO] Experimental version ${{ inputs.experimentalversion }}" - echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEXPERIMENTAL_VERSION=${{ inputs.experimentalversion }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + echo "[INFO] Version ${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}" + echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEMULATOR_VERSION_MAJOR=${{ inputs.next_version_major }} -DEMULATOR_VERSION_MINOR=${{ inputs.next_version_minor }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - name: "Setup cmake" uses: jwlawson/actions-setup-cmake@v2 @@ -184,12 +167,10 @@ jobs: cmake --build . --config ${{ env.BUILD_MODE }} - name: Prepare artifact - if: ${{ inputs.deploymode == 'release' }} run: Rename-Item bin/Cemu_release.exe Cemu.exe - name: Upload artifact uses: actions/upload-artifact@v4 - if: ${{ inputs.deploymode == 'release' }} with: name: cemu-bin-windows-x64 path: ./bin/Cemu.exe @@ -202,24 +183,17 @@ jobs: with: submodules: "recursive" - - name: Setup release mode parameters (for deploy) - if: ${{ inputs.deploymode == 'release' }} + - name: Setup release mode parameters run: | echo "BUILD_MODE=release" >> $GITHUB_ENV echo "BUILD_FLAGS=" >> $GITHUB_ENV echo "Build mode is release" - - name: Setup debug mode parameters (for continous build) - if: ${{ inputs.deploymode != 'release' }} + + - name: Setup build flags for version + if: ${{ inputs.next_version_major != '' }} run: | - echo "BUILD_MODE=debug" >> $GITHUB_ENV - echo "BUILD_FLAGS=" >> $GITHUB_ENV - echo "Build mode is debug" - - - name: Setup version for experimental - if: ${{ inputs.experimentalversion != '' }} - run: | - echo "[INFO] Experimental version ${{ inputs.experimentalversion }}" - echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEXPERIMENTAL_VERSION=${{ inputs.experimentalversion }}" >> $GITHUB_ENV + echo "[INFO] Version ${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}" + echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEMULATOR_VERSION_MAJOR=${{ inputs.next_version_major }} -DEMULATOR_VERSION_MINOR=${{ inputs.next_version_minor }}" >> $GITHUB_ENV - name: "Install system dependencies" run: | @@ -275,7 +249,6 @@ jobs: cmake --build build - name: Prepare artifact - if: ${{ inputs.deploymode == 'release' }} run: | mkdir bin/Cemu_app mv bin/Cemu_release.app bin/Cemu_app/Cemu.app @@ -289,7 +262,6 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 - if: ${{ inputs.deploymode == 'release' }} with: name: cemu-bin-macos-x64 path: ./bin/Cemu.dmg diff --git a/.github/workflows/build_check.yml b/.github/workflows/build_check.yml index 49ef79e9..5d24b0c6 100644 --- a/.github/workflows/build_check.yml +++ b/.github/workflows/build_check.yml @@ -16,6 +16,3 @@ on: jobs: build: uses: ./.github/workflows/build.yml - with: - deploymode: release - experimentalversion: 999999 diff --git a/.github/workflows/deploy_experimental_release.yml b/.github/workflows/deploy_experimental_release.yml index a8c5ec53..97e0c69e 100644 --- a/.github/workflows/deploy_experimental_release.yml +++ b/.github/workflows/deploy_experimental_release.yml @@ -1,20 +1,83 @@ name: Deploy experimental release on: workflow_dispatch: + inputs: + changelog0: + description: 'Enter the changelog lines for this release. Each line is a feature / bullet point. Do not use dash.' + required: true + type: string + changelog1: + description: 'Feature 2' + required: false + type: string + changelog2: + description: 'Feature 3' + required: false + type: string + changelog3: + description: 'Feature 4' + required: false + type: string + changelog4: + description: 'Feature 5' + required: false + type: string + changelog5: + description: 'Feature 6' + required: false + type: string + changelog6: + description: 'Feature 7' + required: false + type: string + changelog7: + description: 'Feature 8' + required: false + type: string + changelog8: + description: 'Feature 9' + required: false + type: string + changelog9: + description: 'Feature 10' + required: false + type: string jobs: + calculate-version: + name: Calculate Version + uses: ./.github/workflows/determine_release_version.yml call-release-build: uses: ./.github/workflows/build.yml + needs: calculate-version with: - deploymode: release - experimentalversion: ${{ github.run_number }} + next_version_major: ${{ needs.calculate-version.outputs.next_version_major }} + next_version_minor: ${{ needs.calculate-version.outputs.next_version_minor }} deploy: name: Deploy experimental release runs-on: ubuntu-22.04 - needs: call-release-build + needs: [call-release-build, calculate-version] steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate changelog + id: generate_changelog + run: | + CHANGELOG="" + if [ -n "${{ github.event.inputs.changelog0 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog0 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog1 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog1 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog2 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog2 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog3 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog3 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog4 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog4 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog5 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog5 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog6 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog6 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog7 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog7 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog8 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog8 }}\n"; fi + if [ -n "${{ github.event.inputs.changelog9 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog9 }}\n"; fi + echo -e "$CHANGELOG" + echo "RELEASE_BODY=$CHANGELOG" >> $GITHUB_ENV - uses: actions/download-artifact@v4 with: name: cemu-bin-linux-x64 @@ -40,15 +103,13 @@ jobs: mkdir upload sudo apt install zip - - name: Get version + - name: Set version dependent vars run: | - echo "Experimental version: ${{ github.run_number }}" - ls - gcc -o getversion .github/getversion.cpp - ./getversion - echo "Cemu CI version: $(./getversion)" - echo "CEMU_FOLDER_NAME=Cemu_$(./getversion)-${{ github.run_number }}" >> $GITHUB_ENV - echo "CEMU_VERSION=$(./getversion)-${{ github.run_number }}" >> $GITHUB_ENV + echo "Version: ${{ needs.calculate-version.outputs.next_version }}" + echo "CEMU_FOLDER_NAME=Cemu_${{ needs.calculate-version.outputs.next_version }}" + echo "CEMU_VERSION=${{ needs.calculate-version.outputs.next_version }}" + echo "CEMU_FOLDER_NAME=Cemu_${{ needs.calculate-version.outputs.next_version }}" >> $GITHUB_ENV + echo "CEMU_VERSION=${{ needs.calculate-version.outputs.next_version }}" >> $GITHUB_ENV - name: Create release from windows-bin run: | @@ -83,4 +144,8 @@ jobs: wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.15.0/ghr_v0.15.0_linux_amd64.tar.gz tar xvzf ghr.tar.gz; rm ghr.tar.gz echo "[INFO] Release tag: v${{ env.CEMU_VERSION }}" - ghr_v0.15.0_linux_amd64/ghr -prerelease -t ${{ secrets.GITHUB_TOKEN }} -n "Cemu ${{ env.CEMU_VERSION }} (Experimental)" -b "Cemu experimental release" "v${{ env.CEMU_VERSION }}" ./upload + CHANGELOG_UNESCAPED=$(printf "%s\n" "${{ env.RELEASE_BODY }}" | sed 's/\\n/\n/g') + RELEASE_BODY=$(printf "%s\n%s" \ + "**Changelog:**" \ + "$CHANGELOG_UNESCAPED") + ghr_v0.15.0_linux_amd64/ghr -draft -t ${{ secrets.GITHUB_TOKEN }} -n "Cemu ${{ env.CEMU_VERSION }}" -b "$RELEASE_BODY" "v${{ env.CEMU_VERSION }}" ./upload diff --git a/.github/workflows/determine_release_version.yml b/.github/workflows/determine_release_version.yml new file mode 100644 index 00000000..be606941 --- /dev/null +++ b/.github/workflows/determine_release_version.yml @@ -0,0 +1,74 @@ +name: Calculate Next Version from release history + +on: + workflow_dispatch: + workflow_call: + outputs: + next_version: + description: "The next semantic version" + value: ${{ jobs.calculate-version.outputs.next_version }} + next_version_major: + description: "The next semantic version (major)" + value: ${{ jobs.calculate-version.outputs.next_version_major }} + next_version_minor: + description: "The next semantic version (minor)" + value: ${{ jobs.calculate-version.outputs.next_version_minor }} + +jobs: + calculate-version: + runs-on: ubuntu-latest + outputs: + next_version: ${{ steps.calculate_next_version.outputs.next_version }} + next_version_major: ${{ steps.calculate_next_version.outputs.next_version_major }} + next_version_minor: ${{ steps.calculate_next_version.outputs.next_version_minor }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Get all releases + id: get_all_releases + run: | + # Fetch all releases and check for API errors + RESPONSE=$(curl -s -o response.json -w "%{http_code}" "https://api.github.com/repos/${{ github.repository }}/releases?per_page=100") + if [ "$RESPONSE" -ne 200 ]; then + echo "Failed to fetch releases. HTTP status: $RESPONSE" + cat response.json + exit 1 + fi + + # Extract and sort tags + ALL_TAGS=$(jq -r '.[].tag_name' response.json | grep -E '^v[0-9]+\.[0-9]+(-[0-9]+)?$' | sed 's/-.*//' | sort -V | tail -n 1) + + # Exit if no tags were found + if [ -z "$ALL_TAGS" ]; then + echo "No valid tags found." + exit 1 + fi + + echo "::set-output name=tag::$ALL_TAGS" + # echo "tag=$ALL_TAGS" >> $GITHUB_STATE + + - name: Calculate next semver minor + id: calculate_next_version + run: | + LATEST_VERSION=${{ steps.get_all_releases.outputs.tag }} + + # strip 'v' prefix and split into major.minor + LATEST_VERSION=${LATEST_VERSION//v/} + IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_VERSION" + + MAJOR=${VERSION_PARTS[0]} + MINOR=${VERSION_PARTS[1]} + + # increment the minor version + MINOR=$((MINOR + 1)) + + NEXT_VERSION="${MAJOR}.${MINOR}" + + echo "Major: $MAJOR" + echo "Minor: $MINOR" + + echo "Next version: $NEXT_VERSION" + echo "::set-output name=next_version::$NEXT_VERSION" + echo "::set-output name=next_version_major::$MAJOR" + echo "::set-output name=next_version_minor::$MINOR" \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 48e18637..80ac6cf0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,18 +2,19 @@ cmake_minimum_required(VERSION 3.21.1) option(ENABLE_VCPKG "Enable the vcpkg package manager" ON) option(MACOS_BUNDLE "The executable when built on macOS will be created as an application bundle" OFF) -set(EXPERIMENTAL_VERSION "" CACHE STRING "") # used by CI script to set experimental version -if (EXPERIMENTAL_VERSION) - add_definitions(-DEMULATOR_VERSION_MINOR=${EXPERIMENTAL_VERSION}) - execute_process( - COMMAND git log --format=%h -1 - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} - OUTPUT_VARIABLE GIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - add_definitions(-DEMULATOR_HASH=${GIT_HASH}) -endif() +# used by CI script to set version: +set(EMULATOR_VERSION_MAJOR "0" CACHE STRING "") +set(EMULATOR_VERSION_MINOR "0" CACHE STRING "") +set(EMULATOR_VERSION_PATCH "0" CACHE STRING "") + +execute_process( + COMMAND git log --format=%h -1 + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + OUTPUT_VARIABLE GIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE +) +add_definitions(-DEMULATOR_HASH=${GIT_HASH}) if (ENABLE_VCPKG) # check if vcpkg is shallow and unshallow it if necessary @@ -62,6 +63,10 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_compile_definitions($<$:CEMU_DEBUG_ASSERT>) # if build type is debug, set CEMU_DEBUG_ASSERT +add_definitions(-DEMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR}) +add_definitions(-DEMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR}) +add_definitions(-DEMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}) + set_property(GLOBAL PROPERTY USE_FOLDERS ON) # enable link time optimization for release builds diff --git a/src/Cafe/HW/Latte/Core/LatteShader.cpp b/src/Cafe/HW/Latte/Core/LatteShader.cpp index b59702cd..77f16468 100644 --- a/src/Cafe/HW/Latte/Core/LatteShader.cpp +++ b/src/Cafe/HW/Latte/Core/LatteShader.cpp @@ -524,7 +524,7 @@ void LatteSHRC_UpdateGSBaseHash(uint8* geometryShaderPtr, uint32 geometryShaderS // update hash from geometry shader data uint64 gsHash1 = 0; uint64 gsHash2 = 0; - _calculateShaderProgramHash((uint32*)geometryShaderPtr, geometryShaderSize, &hashCacheVS, &gsHash1, &gsHash2); + _calculateShaderProgramHash((uint32*)geometryShaderPtr, geometryShaderSize, &hashCacheGS, &gsHash1, &gsHash2); // get geometry shader uint64 gsHash = gsHash1 + gsHash2; gsHash += (uint64)_activeVertexShader->ringParameterCount; diff --git a/src/Cafe/HW/Latte/Renderer/RendererShader.cpp b/src/Cafe/HW/Latte/Renderer/RendererShader.cpp index f66dc9f4..23c8d0ea 100644 --- a/src/Cafe/HW/Latte/Renderer/RendererShader.cpp +++ b/src/Cafe/HW/Latte/Renderer/RendererShader.cpp @@ -12,9 +12,9 @@ uint32 RendererShader::GeneratePrecompiledCacheId() v += (uint32)(*s); s++; } - v += (EMULATOR_VERSION_LEAD * 1000000u); - v += (EMULATOR_VERSION_MAJOR * 10000u); - v += (EMULATOR_VERSION_MINOR * 100u); + v += (EMULATOR_VERSION_MAJOR * 1000000u); + v += (EMULATOR_VERSION_MINOR * 10000u); + v += (EMULATOR_VERSION_PATCH * 100u); // settings that can influence shaders v += (uint32)g_current_game_profile->GetAccurateShaderMul() * 133; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index fb54a803..f464c7a3 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -125,7 +125,7 @@ std::vector VulkanRenderer::GetDevices() VkApplicationInfo app_info{}; app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; app_info.pApplicationName = EMULATOR_NAME; - app_info.applicationVersion = VK_MAKE_VERSION(EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR); + app_info.applicationVersion = VK_MAKE_VERSION(EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH); app_info.pEngineName = EMULATOR_NAME; app_info.engineVersion = app_info.applicationVersion; app_info.apiVersion = apiVersion; @@ -339,7 +339,7 @@ VulkanRenderer::VulkanRenderer() VkApplicationInfo app_info{}; app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; app_info.pApplicationName = EMULATOR_NAME; - app_info.applicationVersion = VK_MAKE_VERSION(EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR); + app_info.applicationVersion = VK_MAKE_VERSION(EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH); app_info.pEngineName = EMULATOR_NAME; app_info.engineVersion = app_info.applicationVersion; app_info.apiVersion = apiVersion; diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Spinlock.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Spinlock.cpp index 4c55c2b0..5201d441 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Spinlock.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Spinlock.cpp @@ -140,7 +140,7 @@ namespace coreinit // we are in single-core mode and the lock will never be released unless we let other threads resume work // to avoid an infinite loop we have no choice but to yield the thread even it is in an uninterruptible state if( !OSIsInterruptEnabled() ) - cemuLog_log(LogType::APIErrors, "OSUninterruptibleSpinLock_Acquire(): Lock is occupied which requires a wait but current thread is already in an uninterruptible state (Avoid cascaded OSDisableInterrupts and/or OSUninterruptibleSpinLock)"); + cemuLog_logOnce(LogType::APIErrors, "OSUninterruptibleSpinLock_Acquire(): Lock is occupied which requires a wait but current thread is already in an uninterruptible state (Avoid cascaded OSDisableInterrupts and/or OSUninterruptibleSpinLock)"); while (!spinlock->ownerThread.atomic_compare_exchange(nullptr, currentThread)) { OSYieldThread(); diff --git a/src/Common/version.h b/src/Common/version.h index 8c08f238..36925a52 100644 --- a/src/Common/version.h +++ b/src/Common/version.h @@ -1,36 +1,19 @@ #ifndef EMULATOR_NAME #define EMULATOR_NAME "Cemu" -#define EMULATOR_VERSION_LEAD 2 -#define EMULATOR_VERSION_MAJOR 0 -// the minor version is used for experimental builds to indicate the build index. Set by command line option from CI build script -// if zero, the version text will be constructed as LEAD.MAJOR, otherwise as LEAD.MAJOR-MINOR - -#if defined(EMULATOR_VERSION_MINOR) && EMULATOR_VERSION_MINOR == 0 #define EMULATOR_VERSION_SUFFIX "" -#else -#define EMULATOR_VERSION_SUFFIX " (experimental)" -#endif - -#ifndef EMULATOR_VERSION_MINOR -#define EMULATOR_VERSION_MINOR 0 -#endif #define _XSTRINGFY(s) _STRINGFY(s) #define _STRINGFY(s) #s -#if EMULATOR_VERSION_MINOR != 0 -#if defined(EMULATOR_HASH) && EMULATOR_VERSION_MINOR == 999999 -#define BUILD_VERSION_WITH_NAME_STRING (EMULATOR_NAME " " _XSTRINGFY(EMULATOR_VERSION_LEAD) "." _XSTRINGFY(EMULATOR_VERSION_MAJOR) "-" _XSTRINGFY(EMULATOR_HASH) EMULATOR_VERSION_SUFFIX) -#define BUILD_VERSION_STRING (_XSTRINGFY(EMULATOR_VERSION_LEAD) "." _XSTRINGFY(EMULATOR_VERSION_MAJOR) "-" _XSTRINGFY(EMULATOR_HASH) EMULATOR_VERSION_SUFFIX) +#if EMULATOR_VERSION_MAJOR != 0 +#define BUILD_VERSION_WITH_NAME_STRING (EMULATOR_NAME " " _XSTRINGFY(EMULATOR_VERSION_MAJOR) "." _XSTRINGFY(EMULATOR_VERSION_MINOR) EMULATOR_VERSION_SUFFIX) +#define BUILD_VERSION_STRING (_XSTRINGFY(EMULATOR_VERSION_MAJOR) "." _XSTRINGFY(EMULATOR_VERSION_MINOR) EMULATOR_VERSION_SUFFIX) #else -#define BUILD_VERSION_WITH_NAME_STRING (EMULATOR_NAME " " _XSTRINGFY(EMULATOR_VERSION_LEAD) "." _XSTRINGFY(EMULATOR_VERSION_MAJOR) "-" _XSTRINGFY(EMULATOR_VERSION_MINOR) EMULATOR_VERSION_SUFFIX) -#define BUILD_VERSION_STRING (_XSTRINGFY(EMULATOR_VERSION_LEAD) "." _XSTRINGFY(EMULATOR_VERSION_MAJOR) "-" _XSTRINGFY(EMULATOR_VERSION_MINOR) EMULATOR_VERSION_SUFFIX) -#endif -#else -#define BUILD_VERSION_STRING (_XSTRINGFY(EMULATOR_VERSION_LEAD) "." _XSTRINGFY(EMULATOR_VERSION_MAJOR) EMULATOR_VERSION_SUFFIX) -#define BUILD_VERSION_WITH_NAME_STRING (EMULATOR_NAME " " _XSTRINGFY(EMULATOR_VERSION_LEAD) "." _XSTRINGFY(EMULATOR_VERSION_MAJOR) EMULATOR_VERSION_SUFFIX) +// no version provided. Only show commit hash +#define BUILD_VERSION_STRING (_XSTRINGFY(EMULATOR_HASH) EMULATOR_VERSION_SUFFIX) +#define BUILD_VERSION_WITH_NAME_STRING (EMULATOR_NAME " " _XSTRINGFY(EMULATOR_HASH) EMULATOR_VERSION_SUFFIX) #endif #endif diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 338392dd..e7920e84 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -38,6 +38,7 @@ void CemuConfig::Load(XMLConfigParser& parser) fullscreen_menubar = parser.get("fullscreen_menubar", false); feral_gamemode = parser.get("feral_gamemode", false); check_update = parser.get("check_update", check_update); + receive_untested_updates = parser.get("receive_untested_updates", check_update); save_screenshot = parser.get("save_screenshot", save_screenshot); did_show_vulkan_warning = parser.get("vk_warning", did_show_vulkan_warning); did_show_graphic_pack_download = parser.get("gp_download", did_show_graphic_pack_download); @@ -360,6 +361,7 @@ void CemuConfig::Save(XMLConfigParser& parser) config.set("fullscreen_menubar", fullscreen_menubar); config.set("feral_gamemode", feral_gamemode); config.set("check_update", check_update); + config.set("receive_untested_updates", receive_untested_updates); config.set("save_screenshot", save_screenshot); config.set("vk_warning", did_show_vulkan_warning); config.set("gp_download", did_show_graphic_pack_download); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 5db8f58c..e2fbb74c 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -413,7 +413,8 @@ struct CemuConfig Vector2i pad_size{ -1,-1 }; ConfigValue pad_maximized; - ConfigValue check_update{false}; + ConfigValue check_update{true}; + ConfigValue receive_untested_updates{false}; ConfigValue save_screenshot{true}; ConfigValue did_show_vulkan_warning{false}; diff --git a/src/config/LaunchSettings.cpp b/src/config/LaunchSettings.cpp index 1731f500..bf38b9cf 100644 --- a/src/config/LaunchSettings.cpp +++ b/src/config/LaunchSettings.cpp @@ -112,10 +112,10 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) { requireConsole(); std::string versionStr; -#if EMULATOR_VERSION_MINOR == 0 - versionStr = fmt::format("{}.{}{}", EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_SUFFIX); +#if EMULATOR_VERSION_PATCH == 0 + versionStr = fmt::format("{}.{}{}", EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_SUFFIX); #else - versionStr = fmt::format("{}.{}-{}{}", EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_SUFFIX); + versionStr = fmt::format("{}.{}-{}{}", EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH, EMULATOR_VERSION_SUFFIX); #endif std::cout << versionStr << std::endl; return false; // exit in main diff --git a/src/gui/CemuUpdateWindow.cpp b/src/gui/CemuUpdateWindow.cpp index 445c7c17..6a4e6885 100644 --- a/src/gui/CemuUpdateWindow.cpp +++ b/src/gui/CemuUpdateWindow.cpp @@ -116,9 +116,11 @@ bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& #elif BOOST_OS_MACOS urlStr.append("&platform=macos_bundle_x86"); #elif - #error Name for current platform is missing #endif + const auto& config = GetConfig(); + if(config.receive_untested_updates) + urlStr.append("&allowNewUpdates=1"); curl_easy_setopt(curl, CURLOPT_URL, urlStr.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); diff --git a/src/gui/DownloadGraphicPacksWindow.cpp b/src/gui/DownloadGraphicPacksWindow.cpp index 03f102d2..9ea9e1dd 100644 --- a/src/gui/DownloadGraphicPacksWindow.cpp +++ b/src/gui/DownloadGraphicPacksWindow.cpp @@ -115,7 +115,7 @@ void DownloadGraphicPacksWindow::UpdateThread() curlDownloadFileState_t tempDownloadState; std::string queryUrl("https://cemu.info/api2/query_graphicpack_url.php?"); char temp[64]; - sprintf(temp, "version=%d.%d.%d", EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR); + sprintf(temp, "version=%d.%d.%d", EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH); queryUrl.append(temp); queryUrl.append("&"); sprintf(temp, "t=%u", (uint32)std::chrono::seconds(std::time(NULL)).count()); // add a dynamic part to the url to bypass overly aggressive caching (like some proxies do) diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index 08395cd3..bd394479 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -141,49 +141,66 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) second_row->SetFlexibleDirection(wxBOTH); second_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); + sint32 checkboxCount = 0; + auto CountRowElement = [&]() + { + checkboxCount++; + if(checkboxCount != 2) + return; + second_row->AddSpacer(10); + checkboxCount = 0; + }; + + auto InsertEmptyRow = [&]() + { + while(checkboxCount != 0) + CountRowElement(); + second_row->AddSpacer(10); + second_row->AddSpacer(10); + second_row->AddSpacer(10); + }; + const int topflag = wxALIGN_CENTER_VERTICAL | wxALL; m_save_window_position_size = new wxCheckBox(box, wxID_ANY, _("Remember main window position")); m_save_window_position_size->SetToolTip(_("Restores the last known window position and size when starting Cemu")); second_row->Add(m_save_window_position_size, 0, topflag, 5); - second_row->AddSpacer(10); + CountRowElement(); + //second_row->AddSpacer(10); m_save_padwindow_position_size = new wxCheckBox(box, wxID_ANY, _("Remember pad window position")); m_save_padwindow_position_size->SetToolTip(_("Restores the last known pad window position and size when opening it")); second_row->Add(m_save_padwindow_position_size, 0, topflag, 5); + CountRowElement(); const int botflag = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT | wxBOTTOM; m_discord_presence = new wxCheckBox(box, wxID_ANY, _("Discord Presence")); m_discord_presence->SetToolTip(_("Enables the Discord Rich Presence feature\nYou will also need to enable it in the Discord settings itself!")); second_row->Add(m_discord_presence, 0, botflag, 5); + CountRowElement(); #ifndef ENABLE_DISCORD_RPC m_discord_presence->Disable(); #endif - second_row->AddSpacer(10); + //second_row->AddSpacer(10); m_fullscreen_menubar = new wxCheckBox(box, wxID_ANY, _("Fullscreen menu bar")); m_fullscreen_menubar->SetToolTip(_("Displays the menu bar when Cemu is running in fullscreen mode and the mouse cursor is moved to the top")); second_row->Add(m_fullscreen_menubar, 0, botflag, 5); + CountRowElement(); - m_auto_update = new wxCheckBox(box, wxID_ANY, _("Automatically check for updates")); - m_auto_update->SetToolTip(_("Automatically checks for new cemu versions on startup")); - second_row->Add(m_auto_update, 0, botflag, 5); -#if BOOST_OS_LINUX - if (!std::getenv("APPIMAGE")) { - m_auto_update->Disable(); - } -#endif - second_row->AddSpacer(10); m_save_screenshot = new wxCheckBox(box, wxID_ANY, _("Save screenshot")); m_save_screenshot->SetToolTip(_("Pressing the screenshot key (F12) will save a screenshot directly to the screenshots folder")); second_row->Add(m_save_screenshot, 0, botflag, 5); + CountRowElement(); m_disable_screensaver = new wxCheckBox(box, wxID_ANY, _("Disable screen saver")); m_disable_screensaver->SetToolTip(_("Prevents the system from activating the screen saver or going to sleep while running a game.")); second_row->Add(m_disable_screensaver, 0, botflag, 5); + CountRowElement(); // Enable/disable feral interactive gamemode #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) m_feral_gamemode = new wxCheckBox(box, wxID_ANY, _("Enable Feral GameMode")); m_feral_gamemode->SetToolTip(_("Use FeralInteractive GameMode if installed.")); second_row->Add(m_feral_gamemode, 0, botflag, 5); + CountRowElement(); #endif // temporary workaround because feature crashes on macOS @@ -191,6 +208,22 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) m_disable_screensaver->Enable(false); #endif + // InsertEmptyRow(); + + m_auto_update = new wxCheckBox(box, wxID_ANY, _("Automatically check for updates")); + m_auto_update->SetToolTip(_("Automatically checks for new cemu versions on startup")); + second_row->Add(m_auto_update, 0, botflag, 5); + CountRowElement(); + + m_receive_untested_releases = new wxCheckBox(box, wxID_ANY, _("Receive untested updates")); + m_receive_untested_releases->SetToolTip(_("When checking for updates, include brand new and untested releases. These may contain bugs!")); + second_row->Add(m_receive_untested_releases, 0, botflag, 5); +#if BOOST_OS_LINUX + if (!std::getenv("APPIMAGE")) { + m_auto_update->Disable(); + } +#endif + box_sizer->Add(second_row, 0, wxEXPAND, 5); } @@ -1536,6 +1569,7 @@ void GeneralSettings2::ApplyConfig() m_fullscreen_menubar->SetValue(config.fullscreen_menubar); m_auto_update->SetValue(config.check_update); + m_receive_untested_releases->SetValue(config.receive_untested_updates); m_save_screenshot->SetValue(config.save_screenshot); m_disable_screensaver->SetValue(config.disable_screensaver); diff --git a/src/gui/GeneralSettings2.h b/src/gui/GeneralSettings2.h index a3429fa1..b1ab01e8 100644 --- a/src/gui/GeneralSettings2.h +++ b/src/gui/GeneralSettings2.h @@ -41,7 +41,7 @@ private: wxCheckBox* m_save_window_position_size; wxCheckBox* m_save_padwindow_position_size; wxCheckBox* m_discord_presence, *m_fullscreen_menubar; - wxCheckBox* m_auto_update, *m_save_screenshot; + wxCheckBox* m_auto_update, *m_receive_untested_releases, *m_save_screenshot; wxCheckBox* m_disable_screensaver; #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) wxCheckBox* m_feral_gamemode; diff --git a/src/resource/cemu.rc b/src/resource/cemu.rc index 860ca8fb..6f78bfc3 100644 --- a/src/resource/cemu.rc +++ b/src/resource/cemu.rc @@ -73,8 +73,8 @@ END #define str(s) #s VS_VERSION_INFO VERSIONINFO -FILEVERSION EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, 0 -PRODUCTVERSION EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, 0 +FILEVERSION EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH, 0 +PRODUCTVERSION EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH, 0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -94,7 +94,7 @@ BEGIN VALUE "LegalCopyright", "Team Cemu" VALUE "OriginalFilename", "Cemu.exe" VALUE "ProductName", "Cemu" - VALUE "ProductVersion", xstr(EMULATOR_VERSION_LEAD) "." xstr(EMULATOR_VERSION_MAJOR) "." xstr(EMULATOR_VERSION_MINOR) EMULATOR_VERSION_SUFFIX "\0" + VALUE "ProductVersion", xstr(EMULATOR_VERSION_MAJOR) "." xstr(EMULATOR_VERSION_MINOR) "." xstr(EMULATOR_VERSION_PATCH) EMULATOR_VERSION_SUFFIX "\0" END END BLOCK "VarFileInfo" From 03484d214678cd5e442f196bbfc38b79c53509f9 Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Wed, 28 Aug 2024 09:05:50 +0000 Subject: [PATCH 015/137] Update translation files --- bin/resources/fr/cemu.mo | Bin 72178 -> 73978 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/fr/cemu.mo b/bin/resources/fr/cemu.mo index 8328991f01eb915b5ea722452ccefde985d6416c..f3f3b49871a8078edf991f90f995d71f14e80f94 100644 GIT binary patch delta 25041 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

6St{+jX!tOk4R)*ctg7C=)jM%9y?j)W}9dO-(+O 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 delta 23379 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 From 9a53b19403d4fea5861bcdf115a673d801759245 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 28 Aug 2024 02:06:49 -0700 Subject: [PATCH 016/137] CI+build: Improve macOS builds (#1310) --- .github/workflows/build.yml | 20 +++++------- BUILD.md | 50 ++++++++++++++++++------------ CMakeLists.txt | 1 + dependencies/ih264d/CMakeLists.txt | 12 +++++-- src/asm/CMakeLists.txt | 12 +++++-- 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd28ceb5..72bbcf52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -176,7 +176,7 @@ jobs: path: ./bin/Cemu.exe build-macos: - runs-on: macos-12 + runs-on: macos-14 steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -198,17 +198,14 @@ jobs: - name: "Install system dependencies" run: | brew update - brew install llvm@15 ninja nasm automake libtool - brew install cmake ninja + brew install ninja nasm automake libtool - - name: "Build and install molten-vk" + - name: "Install molten-vk" run: | - git clone https://github.com/KhronosGroup/MoltenVK.git - cd MoltenVK - git checkout bf097edc74ec3b6dfafdcd5a38d3ce14b11952d6 - ./fetchDependencies --macos - make macos - make install + curl -L -O https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.9/MoltenVK-macos.tar + tar xf MoltenVK-macos.tar + sudo mkdir -p /usr/local/lib + sudo cp MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib /usr/local/lib - name: "Setup cmake" uses: jwlawson/actions-setup-cmake@v2 @@ -239,9 +236,8 @@ jobs: cd build cmake .. ${{ env.BUILD_FLAGS }} \ -DCMAKE_BUILD_TYPE=${{ env.BUILD_MODE }} \ + -DCMAKE_OSX_ARCHITECTURES=x86_64 \ -DMACOS_BUNDLE=ON \ - -DCMAKE_C_COMPILER=/usr/local/opt/llvm@15/bin/clang \ - -DCMAKE_CXX_COMPILER=/usr/local/opt/llvm@15/bin/clang++ \ -G Ninja - name: "Build Cemu" diff --git a/BUILD.md b/BUILD.md index 1e92527e..44d69c6c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -16,11 +16,11 @@ - [Compiling Errors](#compiling-errors) - [Building Errors](#building-errors) - [macOS](#macos) - - [On Apple Silicon Macs, Rosetta 2 and the x86_64 version of Homebrew must be used](#on-apple-silicon-macs-rosetta-2-and-the-x86_64-version-of-homebrew-must-be-used) - [Installing brew](#installing-brew) - - [Installing Dependencies](#installing-dependencies) - - [Build Cemu using CMake and Clang](#build-cemu-using-cmake-and-clang) - - [Updating Cemu and source code](#updating-cemu-and-source-code) + - [Installing Tool Dependencies](#installing-tool-dependencies) + - [Installing Library Dependencies](#installing-library-dependencies) + - [Build Cemu using CMake](#build-cemu-using-cmake) +- [Updating Cemu and source code](#updating-cemu-and-source-code) ## Windows @@ -141,31 +141,41 @@ If you are getting a different error than any of the errors listed above, you ma ## macOS -To compile Cemu, a recent enough compiler and STL with C++20 support is required! LLVM 13 and -below, built in LLVM, and Xcode LLVM don't support the C++20 feature set required. The OpenGL graphics -API isn't support on macOS, Vulkan must be used. Additionally Vulkan must be used through the -Molten-VK compatibility layer - -### On Apple Silicon Macs, Rosetta 2 and the x86_64 version of Homebrew must be used - -You can skip this section if you have an Intel Mac. Every time you compile, you need to perform steps 2. - -1. `softwareupdate --install-rosetta` # Install Rosetta 2 if you don't have it. This only has to be done once -2. `arch -x86_64 zsh` # run an x64 shell +To compile Cemu, a recent enough compiler and STL with C++20 support is required! LLVM 13 and below +don't support the C++20 feature set required, so either install LLVM from Homebrew or make sure that +you have a recent enough version of Xcode. Xcode 15 is known to work. The OpenGL graphics API isn't +supported on macOS, so Vulkan must be used through the Molten-VK compatibility layer. ### Installing brew 1. `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` -2. `eval "$(/usr/local/Homebrew/bin/brew shellenv)"` # set x86_64 brew env +2. Set up the Homebrew shell environment: + 1. **On an Intel Mac:** `eval "$(/usr/local/Homebrew/bin/brew shellenv)"` + 2. **On an Apple Silicon Mac:** eval `"$(/opt/homebrew/bin/brew shellenv)"` -### Installing Dependencies +### Installing Tool Dependencies -`brew install boost git cmake llvm ninja nasm molten-vk automake libtool` +The native versions of these can be used regardless of what type of Mac you have. + +`brew install git cmake ninja nasm automake libtool` + +### Installing Library Dependencies + +**On Apple Silicon Macs, Rosetta 2 and the x86_64 version of Homebrew must be used to install these dependencies:** +1. `softwareupdate --install-rosetta` # Install Rosetta 2 if you don't have it. This only has to be done once +2. `arch -x86_64 zsh` # run an x64 shell +3. `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` +4. `eval "$(/usr/local/Homebrew/bin/brew shellenv)"` + +Then install the dependencies: + +`brew install boost molten-vk` + +### Build Cemu using CMake -### Build Cemu using CMake and Clang 1. `git clone --recursive https://github.com/cemu-project/Cemu` 2. `cd Cemu` -3. `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DCMAKE_C_COMPILER=/usr/local/opt/llvm/bin/clang -DCMAKE_CXX_COMPILER=/usr/local/opt/llvm/bin/clang++ -G Ninja` +3. `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DCMAKE_OSX_ARCHITECTURES=x86_64 -G Ninja` 4. `cmake --build build` 5. You should now have a Cemu executable file in the /bin folder, which you can run using `./bin/Cemu_release`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 80ac6cf0..54e2012a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ endif() if (APPLE) enable_language(OBJC OBJCXX) + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0") endif() if (UNIX AND NOT APPLE) diff --git a/dependencies/ih264d/CMakeLists.txt b/dependencies/ih264d/CMakeLists.txt index d97d6dda..686a9d08 100644 --- a/dependencies/ih264d/CMakeLists.txt +++ b/dependencies/ih264d/CMakeLists.txt @@ -117,7 +117,13 @@ add_library (ih264d "decoder/ivd.h" ) -if (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "amd64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") +if (CMAKE_OSX_ARCHITECTURES) +set(IH264D_ARCHITECTURE ${CMAKE_OSX_ARCHITECTURES}) +else() +set(IH264D_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) +endif() + +if (IH264D_ARCHITECTURE STREQUAL "x86_64" OR IH264D_ARCHITECTURE STREQUAL "amd64" OR IH264D_ARCHITECTURE STREQUAL "AMD64") set(LIBAVCDEC_X86_INCLUDES "common/x86" "decoder/x86") include_directories("common/" "decoder/" ${LIBAVCDEC_X86_INCLUDES}) target_sources(ih264d PRIVATE @@ -140,7 +146,7 @@ target_sources(ih264d PRIVATE "decoder/x86/ih264d_function_selector_sse42.c" "decoder/x86/ih264d_function_selector_ssse3.c" ) -elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") +elseif(IH264D_ARCHITECTURE STREQUAL "aarch64" OR IH264D_ARCHITECTURE STREQUAL "arm64") enable_language( C CXX ASM ) set(LIBAVCDEC_ARM_INCLUDES "common/armv8" "decoder/arm") include_directories("common/" "decoder/" ${LIBAVCDEC_ARM_INCLUDES}) @@ -178,7 +184,7 @@ target_sources(ih264d PRIVATE ) target_compile_options(ih264d PRIVATE -DARMV8) else() -message(FATAL_ERROR "ih264d unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}") +message(FATAL_ERROR "ih264d unknown architecture: ${IH264D_ARCHITECTURE}") endif() if(MSVC) diff --git a/src/asm/CMakeLists.txt b/src/asm/CMakeLists.txt index 5d9f84c2..19a7ddd8 100644 --- a/src/asm/CMakeLists.txt +++ b/src/asm/CMakeLists.txt @@ -1,6 +1,12 @@ project(CemuAsm C) -if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)") +if (CMAKE_OSX_ARCHITECTURES) + set(CEMU_ASM_ARCHITECTURE ${CMAKE_OSX_ARCHITECTURES}) +else() + set(CEMU_ASM_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) +endif() + +if (CEMU_ASM_ARCHITECTURE MATCHES "(x86)|(X86)|(amd64)|(AMD64)") if (WIN32) @@ -40,8 +46,8 @@ if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)") endif() -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "(aarch64)|(AARCH64)") +elseif(CEMU_ASM_ARCHITECTURE MATCHES "(aarch64)|(AARCH64)|(arm64)|(ARM64)") add_library(CemuAsm stub.cpp) else() - message(STATUS "CemuAsm - Unsupported arch: ${CMAKE_SYSTEM_PROCESSOR}") + message(STATUS "CemuAsm - Unsupported arch: ${CEMU_ASM_ARCHITECTURE}") endif() From b06990607d31fc204d82be2b062df4c7edd6e299 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Mon, 2 Sep 2024 15:20:16 +0100 Subject: [PATCH 017/137] nsysnet: Avoid crash on NULL timeout in select (#1324) --- src/Cafe/OS/libs/nsysnet/nsysnet.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp index dd7c9189..5a0ddc59 100644 --- a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp +++ b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp @@ -1210,6 +1210,14 @@ void nsysnetExport_select(PPCInterpreter_t* hCPU) timeval tv = { 0 }; + if (timeOut == NULL) + { + // return immediately + cemuLog_log(LogType::Socket, "select returned immediately because of null timeout"); + osLib_returnFromFunction(hCPU, 0); + return; + } + uint64 msTimeout = (_swapEndianU32(timeOut->tv_usec) / 1000) + (_swapEndianU32(timeOut->tv_sec) * 1000); uint32 startTime = GetTickCount(); while (true) From 0d8fd7c0dc2ea6ff750f4ef5b193ebc6388d31d0 Mon Sep 17 00:00:00 2001 From: MoonlightWave-12 <123384363+MoonlightWave-12@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:22:38 +0200 Subject: [PATCH 018/137] appimage: Do not copy `libstdc++.so.6` to `usr/lib/` (#1319) --- dist/linux/appimage.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/dist/linux/appimage.sh b/dist/linux/appimage.sh index e9081521..b66326d7 100755 --- a/dist/linux/appimage.sh +++ b/dist/linux/appimage.sh @@ -50,7 +50,6 @@ fi echo "Cemu Version Cemu-${GITVERSION}" rm AppDir/usr/lib/libwayland-client.so.0 -cp /lib/x86_64-linux-gnu/libstdc++.so.6 AppDir/usr/lib/ echo -e "export LC_ALL=C\nexport FONTCONFIG_PATH=/etc/fonts" >> AppDir/apprun-hooks/linuxdeploy-plugin-gtk.sh VERSION="${GITVERSION}" ./mkappimage.AppImage --appimage-extract-and-run "${GITHUB_WORKSPACE}"/AppDir From ba54d1540cf6103728abe606223b299d284a6df8 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 8 Sep 2024 17:21:20 +0200 Subject: [PATCH 019/137] Fix "Receive untested updates" option not being synced to config --- src/gui/GeneralSettings2.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index bd394479..eaada7cb 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -932,6 +932,7 @@ void GeneralSettings2::StoreConfig() config.fullscreen_menubar = m_fullscreen_menubar->IsChecked(); config.check_update = m_auto_update->IsChecked(); config.save_screenshot = m_save_screenshot->IsChecked(); + config.receive_untested_updates = m_receive_untested_releases->IsChecked(); #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) config.feral_gamemode = m_feral_gamemode->IsChecked(); #endif From 1a4d9660e756f52a01589add71df619361543a2f Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Sun, 8 Sep 2024 15:40:13 +0000 Subject: [PATCH 020/137] Update translation files --- bin/resources/ko/cemu.mo | Bin 70573 -> 71668 bytes bin/resources/zh/cemu.mo | Bin 58070 -> 60704 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/ko/cemu.mo b/bin/resources/ko/cemu.mo index b812df970a34b41a6d2407c9ec4771adadd6a5db..5ea5e1da1a39c3ac7d15b9b270c5a01a89a4ed07 100644 GIT binary patch delta 22630 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 delta 21570 zcma*t2YeLO!uRo65=ugV(2E3y7J8`CL|UkVfOG}LkZd3jl9+@h;!=WCDK4QXB`96s z29_#d1w}*@L=jZPhLYU~idX@k-+$+X%k@6*`+4T$@q5~wIdx`He)(7Ej?JN-i)BKy zEw1Pg%c_cfDqB{^P|JF%xpFOQa;#;Qz$qAkbFerr!!XP-ZZqyNzKW`M9Ph!iSPHLU zNwm6JR=8z(tWrc&PzCjX1{jL1u?)7yV%QH$<1j3YV^IyInS2jMl3t38!Fmy+@vzDN z2FsED#TeGjvTE~us~QpA7>jCf5Ej9Bq=(j6EQ%9QBc6up$Q&$;t4w+w>b_m5jvv6X z_#UdAFHr-@!@_tS!+5^+hsg-(?mQ?AHFc#?H$r(PR;Uj3M0LDBYUbjwJf@%q zl7;H97xln3sE)j3+=(7Fe2_>Td>8e=tEd})M?I)e4`&aAqaIKRRWAlBVQZ7_kLt+7 zsE#FKC{8lvPZ?)p5%L%HVE&5|$tFV&T!R|nW>fx(Rj$9j~A9xx6EVJd2bM^O(rhjZ{t z?1G~obULyPRqqv4{`<($wj%plR%aZH?QsQahR&j9;0lJ}RSebn|AUAcx{VQ7x}P(J z)$m@@9Z@)+usJ;tPGqn2PXYN~f)BpyfYrSqswcn#HVG1k|k21^l9 z#hR$i(E>GP4`ByP#8$Y`BbgL#U~K1J%)YQA_c?$+w0$^@?F@@=KyVKHaem zjxyyhnEdran13~RgbY3S7?#3QsJ(CzRqn@LcpdBF{X;p+=tgyLIcm3WL#=Th>b~Dm zr^OoP)GKbRj;hzfLu3V!&Zq{@BOBccf0*^gp;!smp_b|ZYH8lX26ze8f#Q!io3%PV zMtU%6>GmMYZCyvrSl8js`(zZVgPu$x>fuwUO)&@W!9`dZw_$ZWiZ*_W>PX=cPQ#UO z2k9i#h$=p6S@&TbRC{q)8poiP@JZBWo{qHRv9gG$Co51(untvm3s%6L#$%{@pQ9T7 z7S*sH>)|cbF^y*U+H?(3?X^bL?}VDU9wvVTM(X^#i8P~NBC6+GFcH7SWb8fCDL;Va zN#~(Xi}ko;X|ze!MD3ketcwFt9hr(%a1QFYzKCk)HH_l<*2hFNrPomfMdF->t0D)( zYK-dnOpL-6s2SRe@puTkWA#zaCQU@mOfIV4o2Wf<9M%5ksG0m3Ju`^>NhArMqBRv< zMy>sis3jxQlG#Ry*o<@yy9yY{H*c{K8bYZu%d7HVJ{}vRC zAR`8sU@LqL>*KH35^dH)4GhNCI2YCM>sSN>#_JeL`cISo8-p`C+L@86sOQDtJveGK z^RI@UCPQnr4K=cTs40IJwW}|p-Uk5;!9TDu-a<9haEvpe9$1TXI%?*YV8@fd1z zPc`}bjhQ zN0#3jhZ@L-PQJ%FM?}ZzORSC8P*Yf*vsVY(qHY+8nzBUH$e%T?!3fejP#t~+^}tgm z{}a>zzCm^TS8Ri!ilg>M6slfZERG#e?L2@bb^eDEDS?ToDa^!D_%w#&BGh?ag<6`IQ6qgDwMj3a z?z@V5z#sSw7Ea@*H!eg@vK9ISFC`p!_JHVf-M2`Pcb8ONJWAL(Rl>EQzj6=iEo4 z8jeLBzkaCt15pi*K<$+{RJ}P^3caY)^%AQ75!C5=8+G56Oy*xB_>m0VcmvgQ*950R z7-|VhqB>d`hhq~|M;4$OT8!Gn%TN#AXxwYckD)qn0=0KO!m{{@hlqNZhvhJIqInRi z;U=j3me>J%p=M|<*2d)~eF*h{cTo>GgF5dYquvLl*$C=z3siY)R6Cw_L}G~a#7;N~ zwPpuU4?d0R>1EWC{Ato*lbt;gg{t2K)uEoK861!0ae_%NK<#=jYNm6Xw8z>`M6bky zsHy%GwItu5dY*?ZFk*@`Bb`v2sT*p<{ZLCX%%u0=2+~JTOHt)Xr(OfpKw6+W*aJ)I z{P!cGwR;TJU@EEulTi=ypiaX)R7V$M1>A{h=oqU0`>6V7Q6s;IdWHXjdIgu6>eOq3 zn%P!p>-_g7avvt6rf7?CFX{nrqGsR(cE#Hmi?L5R<#SMLxfs=vHE81o)QlcO4eUcx ze%_=nVetH4A)?Lp6IR7vQLoIh)10-6MondN)PuU9W~4vrfkRODJ&G0aNn}4;&!I+I z>}jXHDAXQlifU)@)69QOBJpJC0nebOb~!f29atGZ!&-O)wI?bwZ4L20R6TvhTVXb~ z#*^3_L#Okm!`4_0r=U8x5*uLdbSgF_@);T0-4QdKf_tzE>F!t;<4_%)hZ@0llRk}_ z+Fwv>ehW2_vNN3r)$d@iY#=lJm_TL7VhE<2F?N_wWi9UcfBjRkX4DLfYl|)@UMi zdEj2`NcxLsod-lMa(3?{$UoL<{?!N@E_QbLFsx2`2{yxA)G7EHwfieC;X{Odur=-f zjI~KOc+MGcKlI#3Mlz9-xCONo`%zQ&I_d$Zunzu++H~c-&St8E;iTK6?(2bJ*dMj| z9x?f|P&2U*i{jg;c20Sj|4<@dk`aR6qIUZaxCL)x7u>we>2V%v>WgOcqXWAl8{ayP z+6%pxI}Hy&?VWgxzzGG1ZA^MUYOftZ&HM!q5lwZ4RnC;wMor;;sI}^bnzFH| z5vHSNYKn0hY6fSZ>OF&Na1m+-8a?lHumfr-2ch1CnW!c5c!^XZau~HHAER!(jC#=T z$fQ^|l`h2h`vvEB!U~>Jll)In1NaNuVc1&dmE9HZC;b?{g{x5=eSDp>SKP?A!DFQm z(NxVst@S+Al%6zxjT-sys0RK<)ep^a(&4C&OevGDZLE)4q8L>Dwpa!`VhtRCrF8x? ziD-?pP$Ti8Zdi|c;AX6V+p!M5g~jm-s$(}$^=_h;z_s3aL4~2pBT##*ENX^Fp*oy_ z_j+9XmLsBF`s#~L!|$Pbehzi4zBc-ezZh>}Dawnyi65k`fG~g$k0f~nu3|AC0T}QU_EMtyHO83i)tVrbzJ{OjVNrhc`$0~ zYoh8k!$ug38bBJVewN2XoYy7n zfJvx!y{L9xHs$+J^^YSncuk%E8}QX2WQ{_o^P!p5`qEL2(MvrbnS4~umoy^<*+r@H0eiB zU&Ar@JZ?mdw9`(f{$Nyl@uOq%LQ<-lpyxSRhl(C_)Eoy0cpf>ja)WF8=X8t3H%p^k{Sd41WizV=7)C~tw z4?KhF@Of;EVLs;+w8mznhoWX;9;*Ik)Lz($YUc!Yz;949QPZ=>{0RkHkTC_-vmN+2 z9>rQ%XD=TW7e7LA4(UGooFAoUu{7zL`&nmfiR$=Dd)X3Yv;>=_$Dm~md3e~|RC+)E& znS!VBAqr-rruH0aN(&!y_C#w;CEXKC;Q>@fkE53G3~J`S#IpD+s$TeE{x1ktLA|g( zz*xMBU3C6i9C3aUO+@X|Y~u!OMB0bil$TI9{)CJ1-dEYUcnlM_ zzkwR@aa4ytK^re&jE9Kpb?5hbQ)4_ykMeh1ZHl{cM{*G4_) zJ`BNLSQ#J0VmKOg3er&bO~v5%e>M?4aE&S0i|YAn7=>@6-gw`d{D`-l^0KHGRTYzt z#;T+nqn6|W)KUyZE$KLHjjM1Po_>q@f09VQqt05NK%MuSs3|XV%o$lNR0rCj8XSc& zI1%-rjaUVDqdNS)(T_Iiu(zH28lXDZ0kv0#zs>w>4b#ccjc;KS{0g;sO25N*0Bhhd zT!_i|9csn~9CxO2EUMv5tcFWXelBvrtXFUfX1wdnTt3zy{fmc)8me%@xiQB0Al^fM zGOFTSEP~rmYrG5V<7?Owub`&3+I!Ak8iE?gJd-|%+ROoLiqft+_%mv=S)V#nTMa9aY>uU{7gofFF&CdeEn&^`{Ix55 z5g*6W7uX9f-Ycm4-uTSfe5bJv>EfR|&u!)*qMo)v^{@--*bFok(oOjklb(fTDW8v; ziM6QuFJm~KK(+U=DgP2{kq)2+8u^9Op^B*bp1MSa6KRg>z#`NxU1uu1gt~DjHp9KB zdf%fu@;hozL|k+ltc!YKHAmey$T%FesYjV~rlZH2N<TFue7GqeWlTmb)kM^$UW97@AnN#@!y;O|BA1*gEP(?lXoMQsbgYNVP$M~l z>cCNxK80OKe~j9MmA*3P+t?X3ux{7_hu}z@kD9SOw0XW&_-m)9(WnP^FzErtu~?1# znWzV?M|J3JR73BZ^be@re${x(So|C31yvcfXL_JIHV8d>@wka}z!|89Uq|iQcTD;m z4kdjN)j+FnogbmGsB`}w>b^@D+#9Hw`W-c(Lf<(*L@J;<*c4UX;XCGE89m8R14FPD zK8jUw2C96maXYF5ub|%j?_edoYAklyX{QEi?dxGX?1b%cDyqW=jPGA&{`G*bO~q?C zh;+m6ouAj!u?Xo?s0V*+(qE%C(-qV)D*A&{uZ*!WD&I!k*U;p*!Mdb7qB=6#Lqr{T z3N=;pO?oZr!P|{K)UJO8HL}yV0XPQ|LIbW79-yP!ri20P;h)W|LyL$5gWT`1%haUBO6A^8`qo}F*6bs=sR72NM9r)9vLw|ODNEF9#^4p_2)&uvt_@#_` zuax|S|J05RP%o;p*bJ}Y5bD*q!TR?lWAd;3mnwV*yJ7Ti&UsD7UZl68-UrvQ3`YL$ zSPNCZ6_%jEr!bB5{6Czv{|ggHSNW5rqTGYptZ)3~EMbY8%)i#M*G>LH3C_m)SoW5) zOYg@9q^DvmZo(dzhfT5hZT^=U#^cjGAo_3TXZA+R75v|EXK*Ox#ayo73_XroiYHJr z{Q&h3`QNCXM-*`dkC}~1-;0e^549;L zp*pw}OXD7l$G1@fYf#jsJ>jwX5K#l8u|H152k<0nakPQVbI{{=*}`4;0c&RtZ5EBN>Mc9(QU{0C|T z(WP9$)6g060e?5o(j3GwGYA+$!VjiK3__DTf+)EsVx4sHIIZPBSh=J!fMXmnZn(tz>Ak)P!EbiHP8rkoSI@h-j9WGy>T-l$ZvwW zo}sKK@eiCN=kKnI-xAjAgvC_qN2QmD_c1qey6#-#$!kQYL#Rrpo}pysUFE8{hq5>E zWx`bAJ59OXC65zyO)F%czm^IG*J2adbytJe$>Y^-1xy)V+TgXDI!~DR-SGXf^gk=l z;|PTC&E31!=xX_B-}{d_lfJ` zw`lM)n}tWoUqy(b-7iotEM1wzM^gX1xyQ4aj46bB3Az$Yf#NlYFTx9y-6H-LA&K~U z@_4yf8wk3(kk`$`e?v}f@UqFDYT|t`*U7ftHFhsp|1=772m`s{Lkg!8x|05o^tXgZ z3A%nH?+Wn^#Pf*jt*G}yQG&jHP05SFDEx)Mn>zSv9#3AuwT-+(B=id2%KOLqi^yRb zy`M0YaK4}>&ZF`~ljbcPyy}^B0dJG9w`ak%g7lxJ?n+!v(2ovX`i)Q*&+we!rS*S< zptsfs6fPvB64zU9KS5V6mG$>G3a;M7A0ttSwY_fgifL`A+r|gbEeUD_#SCp`WrF-+qH@E4=G!0>Z#7VCcc9BeT2&< z{SNW=#M@9m0=M)1u__aZr=Z} z1;gpczf9Rh(y_!}Bb;=qSPz-HgUFjnd?{vNK7k*?);pv<<%kq96?I=-;>i@0#T}$) z5^j?ogB8hZOI+8Ngf!CMne-^qpA+=Mq&6L^Kp0HC3*j?@Em}1hL-M{RU4!&) zLLtJ#L(@2}#(Wsq!!`lRjwDpJRXOpE8Y9$328igpS-h3CmDF z#?-$-TGv3zhTw3U6(hIv0;t9pcuR~Z#*hnbM!(y@E zT0kU)FoWaq4#61i7!{ax%nybx+|Xtb|$Y9 z@nn+Gcn|3TD&w*#uZ8KPb-HvtNIV-q!OUO|o053Fg73fbz92+%v#u)?e&s|h7ma^S zx`*jVOX8O)t3_R}$=gD@6(NhVA_QF#gl2^Kq}Q3e(WHlSPa~5K_V95sY?BdbIzT>aGj;osSV!Ks#6Q4uq>mBbZ|Z$TyaRbgkFSF%ti#^2JsA4*8CSIF^v#M(6x;Q9wb~g>5qu7AQW6r63I5{7jZiE#u6SN zd`b9$P;h-hB#t^$$g4m+ig1ebn}iPJ1=nAdtCCT01|}(B>am|m%K*^-H1Pl z1=pj*=Tr9x;W*(BQ}z@UN)U#S{+@7&5Kj7DQ(lf)n4tAfG%fv3!M|u=JpPEf+7rJ@ z{4{Zo$y5Hv`WM%C8`}T#P;|-?jy`6KAZ3g;RNXk1m|M>m5B8vV=p0| z2e!hm2_@;_4AhlIXh-O1(nT?Z@{81OOneDt0|}#u--|s6zfv}vbb0E95;l@vMtGXM zF{tZvZKC=_PMHj4&ZWXdoJHQFSdz4^PjMJl!>W{BAWS3u8)2EL^DOamCQdZ?--6}e zgnf!|ttC|E-jC2Toyt2%gb?Ntr=!+41YIKtKJq6K?!F$QUK_&Af8?u9Z9>vN@;95C zPjXKrWycBY$V<@g|H-DZ8(ULhHwA-Dp6+Wxd=;S@A(`?&$XkFla3Xm}3A#?=U*sRb zP{J_6tAs-YU9~A|KzNq)Po$Rzb6NjdWDKL=MKVeg|B84&yh_L;ZxOyiXiVr&Su|lZ z@p-`pod3W$hrE}`TS#6POeDRN_%-5XzaivOd!LeYg7erKY=+Qj#+52m9Nd`}eYCt zK?im25xk>Q&oQO@4;b9Fzg;geHQf$YtM9$gGdo0G8PltGSZe$@cd&I|Zm(2Vc)irL z%%s$mxa9g?yLYSpcKn2l%+&Gr_~dvyEiQA6otRE4&Q42rPfSXkkYUHV$4{`6QW!~c za-8n8$HZmWqulNkJ27=aN`f6nNK8sjb|+9R#y6<C&i^?+L@`Q2JNL%KRwu+j^4rpfA@A8JkEP^aJvx` z(h}k_-Bz$ilX#-72^>F)x-^siUn5CMvFr5d7u&f{=l=b>b{^EZPLtrBnx+)@r2iOG zMvSk=kfTMqvm`u$;itQwn2?n2^tNd;J7dDA#MI;jp2W=z)?Fh!yjyyDYI-|+YSU@q zgW}RhGsl|0cHvW-J@@E9-|R;(xC%3=O`G{fjqF~i+kcF7ViM2Sq$H$HN=Z(QOW;AH z)8o>{Bxw=i$7OW1x~8Vo&9pPdq^e?4h8;ILE-A&kF|Luf#3AwaA$qE_-NFl0BG7W=D~&3C*yRlg6=2;wHMiRiCXK zmB4;vwd1UT!F|J$n2oAQ_7g)ku%Eq=Fd3g@@35$9^$JzCo8U9?1Q}ykBX(Ho__QSF6vWt_@2+rHi5>5WOVPtdxr3vQ3Ae)s6l|wEPiton={>UB zut6i!lG5DCNh$7;!M#U6M#jg*k0F_wl9`^G93I}wIeU7YsN0&djLd@Va%YI+m} zr}I*BPM8VU;!4XUh-pYH)**?x@N=nIer}J8~6O-dI#sv3S+-P@<-7Ax2OUdN)aEN$k1c#w* zmeHu-6~GpBUM_3>G0}bn+B7D{IXAnq535E;D$X@4PKIze$=S zwzBh<&<5gz^4~MWxJJ~s!+Ry#$?n8V+dV!lb4sJ&a5Q3b_oSpcbd>GmJV5W&jHCpu ztlN1VJN06G?=L#&@;$sXu2>m6&7D3zE=4P+6UNAW`&O+9@#U@gtx#Q7BVC7)7nylN zhTAPRU_by&jJyeI$Yd>2)!Fg#oa~nLMG2y{O?VgL{HMN^I z^_AJM-lcE%rj4z=*Ei;@_@bPvwD;*v_eKSdEDJ2(!$1B7bMxmPu>D(?`L}KHZ`vOG zJU+dtLc~8aBJa+q7ad!FMd$LadZCJWcW=7s{c!W3V$nU!$Gp~-HYEcKRtMHD&Cgz$ zcObW3w0GE+{$bHQ&F9>^c}tDpr}prcZr<8kKWh;EfB$yh-a0)pI{5Lf7yNDqp5I#E z`|h@nN(6GI2i9)X0O~~#^gjP`O4u_C1BboJ@DJ@(IEW(Q`? zwVf^pd$>9)KYP#r_U-1e8cmgdx6X64dgkmX)-A9)%g$fC z$N%gy@9a0*lnwVUoF14p!*tTtC)<1YO}F=_w`^b5TQP;qcYE)#6@_~EXYTRmZ1Cs4 z5Lh$a&f9;$zjkH*vLkl>!mWWdS+?J|hs}|<+stC#!G#?yW`I09f9Yxw$ewHGXK(TL zK3?B@=y*%t_TxoEd{s|ASHxHDqrVD!Z|$k(?RI{6_w~7foa{z6!^xjD-Bz2qGXgm? zZ2!t7nxMSh3j=Gj@|P|2=geiDHSg3n%dd_E_ASfHo$kGQz9vtp5*h5`BmL;mdP z!LNo>mnG7s3)h>i|9SA6V#(x>>bXBfULs|FgN|S z97ZcI}j?6CLR8|6KG>Hpod z)25xjEQ^yD+&Nq9|NB=zu$S$V^Zy!^&T;UFYvQ)DAG9Y{MsoC4vJDvDp1j?;{w2@( z%6SL=*{>RX_WKUpwUoj0o4sCZH-Ce=Ug`gPa9{j$h^vyb2k%;WonCFa$eZOtm^$x6 zH>128Zr1f?-)vpIUSQ>1M*Cks40KOm%}TF*uzWpd&lWUjhLn??pPi%q?O)8)uAR&L zdxzh;75qVP*QQ&s#J_X9a~OE(YyF-5#c|b}QzvEZ%Ha897eAB3wqZSauKdHWCAdHXoq8~-t3Y>o^Nkn zZr1)6i@UxJ**`GSbuvWL&98-jx8dr8oePso5=JH`Ecw<|snQ1F-S8%o5zrKQNpzD7C`pT&m diff --git a/bin/resources/zh/cemu.mo b/bin/resources/zh/cemu.mo index 69e06e572cbc240cd3fa396bb0fb323e9dfb62cd..3e6369713ad379e558631b4db68b02fde8dab833 100644 GIT binary patch delta 18541 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 delta 16136 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 From a05bdb172d198904a76b4b166b43fb31a94a0dd3 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Sun, 15 Sep 2024 20:23:11 +0200 Subject: [PATCH 021/137] Vulkan: Add explicit synchronization on frame boundaries (#1290) --- .../Latte/Renderer/Vulkan/SwapchainInfoVk.cpp | 34 ++++++++- .../Latte/Renderer/Vulkan/SwapchainInfoVk.h | 8 ++ src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h | 3 + .../Latte/Renderer/Vulkan/VulkanRenderer.cpp | 73 ++++++++++++++++++- .../HW/Latte/Renderer/Vulkan/VulkanRenderer.h | 4 +- 5 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/SwapchainInfoVk.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/SwapchainInfoVk.cpp index 75ff02ba..56a3ab12 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/SwapchainInfoVk.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/SwapchainInfoVk.cpp @@ -146,8 +146,17 @@ void SwapchainInfoVk::Create() UnrecoverableError("Failed to create semaphore for swapchain acquire"); } + VkFenceCreateInfo fenceInfo = {}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + result = vkCreateFence(m_logicalDevice, &fenceInfo, nullptr, &m_imageAvailableFence); + if (result != VK_SUCCESS) + UnrecoverableError("Failed to create fence for swapchain"); + m_acquireIndex = 0; hasDefinedSwapchainImage = false; + + m_queueDepth = 0; } void SwapchainInfoVk::Cleanup() @@ -177,6 +186,12 @@ void SwapchainInfoVk::Cleanup() m_swapchainFramebuffers.clear(); + if (m_imageAvailableFence) + { + WaitAvailableFence(); + vkDestroyFence(m_logicalDevice, m_imageAvailableFence, nullptr); + m_imageAvailableFence = nullptr; + } if (m_swapchain) { vkDestroySwapchainKHR(m_logicalDevice, m_swapchain, nullptr); @@ -189,6 +204,18 @@ bool SwapchainInfoVk::IsValid() const return m_swapchain && !m_acquireSemaphores.empty(); } +void SwapchainInfoVk::WaitAvailableFence() +{ + if(m_awaitableFence != VK_NULL_HANDLE) + vkWaitForFences(m_logicalDevice, 1, &m_awaitableFence, VK_TRUE, UINT64_MAX); + m_awaitableFence = VK_NULL_HANDLE; +} + +void SwapchainInfoVk::ResetAvailableFence() const +{ + vkResetFences(m_logicalDevice, 1, &m_imageAvailableFence); +} + VkSemaphore SwapchainInfoVk::ConsumeAcquireSemaphore() { VkSemaphore ret = m_currentSemaphore; @@ -198,8 +225,10 @@ VkSemaphore SwapchainInfoVk::ConsumeAcquireSemaphore() bool SwapchainInfoVk::AcquireImage() { + ResetAvailableFence(); + VkSemaphore acquireSemaphore = m_acquireSemaphores[m_acquireIndex]; - VkResult result = vkAcquireNextImageKHR(m_logicalDevice, m_swapchain, 1'000'000'000, acquireSemaphore, nullptr, &swapchainImageIndex); + VkResult result = vkAcquireNextImageKHR(m_logicalDevice, m_swapchain, 1'000'000'000, acquireSemaphore, m_imageAvailableFence, &swapchainImageIndex); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) m_shouldRecreate = true; if (result == VK_TIMEOUT) @@ -216,6 +245,7 @@ bool SwapchainInfoVk::AcquireImage() return false; } m_currentSemaphore = acquireSemaphore; + m_awaitableFence = m_imageAvailableFence; m_acquireIndex = (m_acquireIndex + 1) % m_swapchainImages.size(); return true; @@ -319,6 +349,7 @@ VkExtent2D SwapchainInfoVk::ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& cap VkPresentModeKHR SwapchainInfoVk::ChoosePresentMode(const std::vector& modes) { + m_maxQueued = 0; const auto vsyncState = (VSync)GetConfig().vsync.GetValue(); if (vsyncState == VSync::MAILBOX) { @@ -345,6 +376,7 @@ VkPresentModeKHR SwapchainInfoVk::ChoosePresentMode(const std::vector m_acquireSemaphores; // indexed by m_acquireIndex + VkFence m_imageAvailableFence{}; + VkFence m_awaitableFence = VK_NULL_HANDLE; VkSemaphore m_currentSemaphore = VK_NULL_HANDLE; std::array m_swapchainQueueFamilyIndices; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h index 0489bb4e..6bde2a0b 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h @@ -188,6 +188,9 @@ VKFUNC_DEVICE(vkCmdPipelineBarrier2KHR); VKFUNC_DEVICE(vkCmdBeginRenderingKHR); VKFUNC_DEVICE(vkCmdEndRenderingKHR); +// khr_present_wait +VKFUNC_DEVICE(vkWaitForPresentKHR); + // transform feedback extension VKFUNC_DEVICE(vkCmdBindTransformFeedbackBuffersEXT); VKFUNC_DEVICE(vkCmdBeginTransformFeedbackEXT); diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index f464c7a3..12d1d975 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -47,7 +47,9 @@ const std::vector kOptionalDeviceExtensions = VK_EXT_FILTER_CUBIC_EXTENSION_NAME, // not supported by any device yet VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, - VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME + VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME, + VK_KHR_PRESENT_WAIT_EXTENSION_NAME, + VK_KHR_PRESENT_ID_EXTENSION_NAME }; const std::vector kRequiredDeviceExtensions = @@ -252,12 +254,24 @@ void VulkanRenderer::GetDeviceFeatures() pcc.pNext = prevStruct; prevStruct = &pcc; + VkPhysicalDevicePresentIdFeaturesKHR pidf{}; + pidf.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR; + pidf.pNext = prevStruct; + prevStruct = &pidf; + + VkPhysicalDevicePresentWaitFeaturesKHR pwf{}; + pwf.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR; + pwf.pNext = prevStruct; + prevStruct = &pwf; + VkPhysicalDeviceFeatures2 physicalDeviceFeatures2{}; physicalDeviceFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; physicalDeviceFeatures2.pNext = prevStruct; vkGetPhysicalDeviceFeatures2(m_physicalDevice, &physicalDeviceFeatures2); + cemuLog_log(LogType::Force, "Vulkan: present_wait extension: {}", (pwf.presentWait && pidf.presentId) ? "supported" : "unsupported"); + /* Get Vulkan device properties and limits */ VkPhysicalDeviceFloatControlsPropertiesKHR pfcp{}; prevStruct = nullptr; @@ -490,6 +504,24 @@ VulkanRenderer::VulkanRenderer() customBorderColorFeature.customBorderColors = VK_TRUE; customBorderColorFeature.customBorderColorWithoutFormat = VK_TRUE; } + // enable VK_KHR_present_id + VkPhysicalDevicePresentIdFeaturesKHR presentIdFeature{}; + if(m_featureControl.deviceExtensions.present_wait) + { + presentIdFeature.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR; + presentIdFeature.pNext = deviceExtensionFeatures; + deviceExtensionFeatures = &presentIdFeature; + presentIdFeature.presentId = VK_TRUE; + } + // enable VK_KHR_present_wait + VkPhysicalDevicePresentWaitFeaturesKHR presentWaitFeature{}; + if(m_featureControl.deviceExtensions.present_wait) + { + presentWaitFeature.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR; + presentWaitFeature.pNext = deviceExtensionFeatures; + deviceExtensionFeatures = &presentWaitFeature; + presentWaitFeature.presentWait = VK_TRUE; + } std::vector used_extensions; VkDeviceCreateInfo createInfo = CreateDeviceCreateInfo(queueCreateInfos, deviceFeatures, deviceExtensionFeatures, used_extensions); @@ -1047,6 +1079,10 @@ VkDeviceCreateInfo VulkanRenderer::CreateDeviceCreateInfo(const std::vector 0); + // wait for the previous frame to finish rendering + WaitCommandBufferFinished(m_commandBufferIDOfPrevFrame); + m_commandBufferIDOfPrevFrame = currentFrameCmdBufferID; + + chainInfo.WaitAvailableFence(); + + VkPresentIdKHR presentId = {}; + VkPresentInfoKHR presentInfo = {}; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.swapchainCount = 1; @@ -2709,6 +2756,24 @@ void VulkanRenderer::SwapBuffer(bool mainWindow) presentInfo.waitSemaphoreCount = 1; presentInfo.pWaitSemaphores = &presentSemaphore; + // if present_wait is available and enabled, add frame markers to present requests + // and limit the number of queued present operations + if (m_featureControl.deviceExtensions.present_wait && chainInfo.m_maxQueued > 0) + { + presentId.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR; + presentId.swapchainCount = 1; + presentId.pPresentIds = &chainInfo.m_presentId; + + presentInfo.pNext = &presentId; + + if(chainInfo.m_queueDepth >= chainInfo.m_maxQueued) + { + uint64 waitFrameId = chainInfo.m_presentId - chainInfo.m_queueDepth; + vkWaitForPresentKHR(m_logicalDevice, chainInfo.m_swapchain, waitFrameId, 40'000'000); + chainInfo.m_queueDepth--; + } + } + VkResult result = vkQueuePresentKHR(m_presentQueue, &presentInfo); if (result < 0 && result != VK_ERROR_OUT_OF_DATE_KHR) { @@ -2717,6 +2782,12 @@ void VulkanRenderer::SwapBuffer(bool mainWindow) if(result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) chainInfo.m_shouldRecreate = true; + if(result >= 0) + { + chainInfo.m_queueDepth++; + chainInfo.m_presentId++; + } + chainInfo.hasDefinedSwapchainImage = false; chainInfo.swapchainImageIndex = -1; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h index 6df53da4..ce97b5e9 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h @@ -450,6 +450,7 @@ private: bool synchronization2 = false; // VK_KHR_synchronization2 bool dynamic_rendering = false; // VK_KHR_dynamic_rendering bool shader_float_controls = false; // VK_KHR_shader_float_controls + bool present_wait = false; // VK_KHR_present_wait }deviceExtensions; struct @@ -457,7 +458,7 @@ private: bool shaderRoundingModeRTEFloat32{ false }; }shaderFloatControls; // from VK_KHR_shader_float_controls - struct + struct { bool debug_utils = false; // VK_EXT_DEBUG_UTILS }instanceExtensions; @@ -635,6 +636,7 @@ private: size_t m_commandBufferIndex = 0; // current buffer being filled size_t m_commandBufferSyncIndex = 0; // latest buffer that finished execution (updated on submit) + size_t m_commandBufferIDOfPrevFrame = 0; std::array m_cmd_buffer_fences; std::array m_commandBuffers; std::array m_commandBufferSemaphores; From adffd53dbdc86f54803e79dcf06004e688e3b19d Mon Sep 17 00:00:00 2001 From: Andrea Toska Date: Mon, 16 Sep 2024 12:40:38 +0200 Subject: [PATCH 022/137] boss: Fix BOSS not honoring the proxy_server setting (#1344) --- src/Cafe/IOSU/legacy/iosu_boss.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Cafe/IOSU/legacy/iosu_boss.cpp b/src/Cafe/IOSU/legacy/iosu_boss.cpp index 760e5b66..7ab25f68 100644 --- a/src/Cafe/IOSU/legacy/iosu_boss.cpp +++ b/src/Cafe/IOSU/legacy/iosu_boss.cpp @@ -137,6 +137,10 @@ namespace iosu this->task_settings.taskType = settings->taskType; curl = std::shared_ptr(curl_easy_init(), curl_easy_cleanup); + if(GetConfig().proxy_server.GetValue() != "") + { + curl_easy_setopt(curl.get(), CURLOPT_PROXY, GetConfig().proxy_server.GetValue().c_str()); + } } }; From 8508c625407e80a5a7fcb9cf02c5355d018ff64b Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 17 Sep 2024 01:00:26 +0100 Subject: [PATCH 023/137] Various smaller code improvements (#1343) --- CMakeLists.txt | 2 +- src/Cafe/OS/libs/nn_olv/nn_olv_OfflineDB.cpp | 4 +- src/Cafe/OS/libs/nsyshid/Infinity.cpp | 6 +- src/Cafe/OS/libs/ntag/ntag.cpp | 2 +- src/Cafe/OS/libs/snd_core/ax_out.cpp | 8 +- src/Common/MemPtr.h | 160 ++++++++++++------- src/Common/precompiled.h | 48 +++--- src/gui/CemuApp.cpp | 3 + src/util/CMakeLists.txt | 1 - src/util/ThreadPool/ThreadPool.cpp | 0 src/util/containers/robin_hood.h | 2 +- src/util/crypto/crc32.cpp | 55 ++----- src/util/crypto/crc32.h | 4 +- 13 files changed, 153 insertions(+), 142 deletions(-) delete mode 100644 src/util/ThreadPool/ThreadPool.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 54e2012a..5b5cff6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,7 +222,7 @@ if (ENABLE_CUBEB) option(BUILD_TOOLS "" OFF) option(BUNDLE_SPEEX "" OFF) set(USE_WINMM OFF CACHE BOOL "") - add_subdirectory("dependencies/cubeb" EXCLUDE_FROM_ALL) + add_subdirectory("dependencies/cubeb" EXCLUDE_FROM_ALL SYSTEM) set_property(TARGET cubeb PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_library(cubeb::cubeb ALIAS cubeb) endif() diff --git a/src/Cafe/OS/libs/nn_olv/nn_olv_OfflineDB.cpp b/src/Cafe/OS/libs/nn_olv/nn_olv_OfflineDB.cpp index 309394e6..c87cbd39 100644 --- a/src/Cafe/OS/libs/nn_olv/nn_olv_OfflineDB.cpp +++ b/src/Cafe/OS/libs/nn_olv/nn_olv_OfflineDB.cpp @@ -112,7 +112,7 @@ namespace nn nnResult _Async_OfflineDB_DownloadPostDataListParam_DownloadPostDataList(coreinit::OSEvent* event, DownloadedTopicData* downloadedTopicData, DownloadedPostData* downloadedPostData, uint32be* postCountOut, uint32 maxCount, DownloadPostDataListParam* param) { - scope_exit _se([&](){coreinit::OSSignalEvent(event);}); + stdx::scope_exit _se([&](){coreinit::OSSignalEvent(event);}); uint64 titleId = CafeSystem::GetForegroundTitleId(); @@ -184,7 +184,7 @@ namespace nn nnResult _Async_OfflineDB_DownloadPostDataListParam_DownloadExternalImageData(coreinit::OSEvent* event, DownloadedDataBase* _this, void* imageDataOut, uint32be* imageSizeOut, uint32 maxSize) { - scope_exit _se([&](){coreinit::OSSignalEvent(event);}); + stdx::scope_exit _se([&](){coreinit::OSSignalEvent(event);}); if (!_this->TestFlags(_this, DownloadedDataBase::FLAGS::HAS_EXTERNAL_IMAGE)) return OLV_RESULT_MISSING_DATA; diff --git a/src/Cafe/OS/libs/nsyshid/Infinity.cpp b/src/Cafe/OS/libs/nsyshid/Infinity.cpp index ab44ef4a..ac793109 100644 --- a/src/Cafe/OS/libs/nsyshid/Infinity.cpp +++ b/src/Cafe/OS/libs/nsyshid/Infinity.cpp @@ -1017,11 +1017,7 @@ namespace nsyshid std::array InfinityUSB::GenerateInfinityFigureKey(const std::vector& sha1Data) { std::array digest = {}; - SHA_CTX ctx; - SHA1_Init(&ctx); - SHA1_Update(&ctx, sha1Data.data(), sha1Data.size()); - SHA1_Final(digest.data(), &ctx); - OPENSSL_cleanse(&ctx, sizeof(ctx)); + SHA1(sha1Data.data(), sha1Data.size(), digest.data()); // Infinity AES keys are the first 16 bytes of the SHA1 Digest, every set of 4 bytes need to be // reversed due to endianness std::array key = {}; diff --git a/src/Cafe/OS/libs/ntag/ntag.cpp b/src/Cafe/OS/libs/ntag/ntag.cpp index 24617791..7c95a1a1 100644 --- a/src/Cafe/OS/libs/ntag/ntag.cpp +++ b/src/Cafe/OS/libs/ntag/ntag.cpp @@ -509,7 +509,7 @@ namespace ntag noftHeader->writeCount = _swapEndianU16(_swapEndianU16(noftHeader->writeCount) + 1); } - memcpy(decryptedBuffer + 0x20, noftHeader, sizeof(noftHeader)); + memcpy(decryptedBuffer + 0x20, noftHeader, sizeof(NTAGNoftHeader)); memcpy(decryptedBuffer + _swapEndianU16(rwHeader->offset), data, dataSize); // Encrypt diff --git a/src/Cafe/OS/libs/snd_core/ax_out.cpp b/src/Cafe/OS/libs/snd_core/ax_out.cpp index 68b05165..40b9c643 100644 --- a/src/Cafe/OS/libs/snd_core/ax_out.cpp +++ b/src/Cafe/OS/libs/snd_core/ax_out.cpp @@ -522,10 +522,10 @@ namespace snd_core // called periodically to check for AX updates void AXOut_update() { - constexpr auto kTimeout = std::chrono::duration_cast(std::chrono::milliseconds(((IAudioAPI::kBlockCount * 3) / 4) * (AX_FRAMES_PER_GROUP * 3))); - constexpr auto kWaitDuration = std::chrono::duration_cast(std::chrono::milliseconds(3)); - constexpr auto kWaitDurationFast = std::chrono::duration_cast(std::chrono::microseconds(2900)); - constexpr auto kWaitDurationMinimum = std::chrono::duration_cast(std::chrono::microseconds(1700)); + constexpr static auto kTimeout = std::chrono::duration_cast(std::chrono::milliseconds(((IAudioAPI::kBlockCount * 3) / 4) * (AX_FRAMES_PER_GROUP * 3))); + constexpr static auto kWaitDuration = std::chrono::duration_cast(std::chrono::milliseconds(3)); + constexpr static auto kWaitDurationFast = std::chrono::duration_cast(std::chrono::microseconds(2900)); + constexpr static auto kWaitDurationMinimum = std::chrono::duration_cast(std::chrono::microseconds(1700)); // if we haven't buffered any blocks, we will wait less time than usual bool additional_blocks_required = false; diff --git a/src/Common/MemPtr.h b/src/Common/MemPtr.h index 142da7e4..7825e4d5 100644 --- a/src/Common/MemPtr.h +++ b/src/Common/MemPtr.h @@ -4,7 +4,7 @@ using MPTR = uint32; // generic address in PowerPC memory space -#define MPTR_NULL (0) +#define MPTR_NULL (0) using VAddr = uint32; // virtual address using PAddr = uint32; // physical address @@ -14,137 +14,175 @@ extern uint8* PPCInterpreterGetStackPointer(); extern uint8* PPCInterpreter_PushAndReturnStackPointer(sint32 offset); extern void PPCInterpreterModifyStackPointer(sint32 offset); -class MEMPTRBase {}; +class MEMPTRBase +{ +}; -template +template class MEMPTR : MEMPTRBase { -public: - constexpr MEMPTR() - : m_value(0) { } + public: + constexpr MEMPTR() noexcept + : m_value(0) {} - explicit constexpr MEMPTR(uint32 offset) - : m_value(offset) { } + explicit constexpr MEMPTR(uint32 offset) noexcept + : m_value(offset) {} - explicit constexpr MEMPTR(const uint32be& offset) - : m_value(offset) { } + explicit constexpr MEMPTR(const uint32be& offset) noexcept + : m_value(offset) {} - constexpr MEMPTR(std::nullptr_t) - : m_value(0) { } + constexpr MEMPTR(std::nullptr_t) noexcept + : m_value(0) {} - MEMPTR(T* ptr) + MEMPTR(T* ptr) noexcept { if (ptr == nullptr) m_value = 0; else - { - cemu_assert_debug((uint8*)ptr >= memory_base && (uint8*)ptr <= memory_base + 0x100000000); - m_value = (uint32)((uintptr_t)ptr - (uintptr_t)memory_base); - } + { + cemu_assert_debug((uint8*)ptr >= memory_base && (uint8*)ptr <= memory_base + 0x100000000); + m_value = (uint32)((uintptr_t)ptr - (uintptr_t)memory_base); + } } - constexpr MEMPTR(const MEMPTR& memptr) - : m_value(memptr.m_value) { } + constexpr MEMPTR(const MEMPTR&) noexcept = default; - constexpr MEMPTR& operator=(const MEMPTR& memptr) - { - m_value = memptr.m_value; - return *this; - } + constexpr MEMPTR& operator=(const MEMPTR&) noexcept = default; - constexpr MEMPTR& operator=(const uint32& offset) + constexpr MEMPTR& operator=(const uint32& offset) noexcept { m_value = offset; return *this; } - constexpr MEMPTR& operator=(const std::nullptr_t rhs) + constexpr MEMPTR& operator=(std::nullptr_t) noexcept { m_value = 0; return *this; } - MEMPTR& operator=(T* ptr) + MEMPTR& operator=(T* ptr) noexcept { - if (ptr == nullptr) + if (ptr == nullptr) m_value = 0; else - { - cemu_assert_debug((uint8*)ptr >= memory_base && (uint8*)ptr <= memory_base + 0x100000000); - m_value = (uint32)((uintptr_t)ptr - (uintptr_t)memory_base); - } + { + cemu_assert_debug((uint8*)ptr >= memory_base && (uint8*)ptr <= memory_base + 0x100000000); + m_value = (uint32)((uintptr_t)ptr - (uintptr_t)memory_base); + } return *this; } - bool atomic_compare_exchange(T* comparePtr, T* newPtr) + bool atomic_compare_exchange(T* comparePtr, T* newPtr) noexcept { MEMPTR mp_compare = comparePtr; MEMPTR mp_new = newPtr; - std::atomic* thisValueAtomic = (std::atomic*)&m_value; + auto* thisValueAtomic = reinterpret_cast*>(&m_value); return thisValueAtomic->compare_exchange_strong(mp_compare.m_value, mp_new.m_value); } - explicit constexpr operator bool() const noexcept { return m_value != 0; } - - constexpr operator T*() const noexcept { return GetPtr(); } // allow implicit cast to wrapped pointer type + explicit constexpr operator bool() const noexcept + { + return m_value != 0; + } + // allow implicit cast to wrapped pointer type + constexpr operator T*() const noexcept + { + return GetPtr(); + } - template - explicit operator MEMPTR() const { return MEMPTR(this->m_value); } + template + explicit operator MEMPTR() const noexcept + { + return MEMPTR(this->m_value); + } - MEMPTR operator+(const MEMPTR& ptr) { return MEMPTR(this->GetMPTR() + ptr.GetMPTR()); } - MEMPTR operator-(const MEMPTR& ptr) { return MEMPTR(this->GetMPTR() - ptr.GetMPTR()); } + MEMPTR operator+(const MEMPTR& ptr) noexcept + { + return MEMPTR(this->GetMPTR() + ptr.GetMPTR()); + } + MEMPTR operator-(const MEMPTR& ptr) noexcept + { + return MEMPTR(this->GetMPTR() - ptr.GetMPTR()); + } - MEMPTR operator+(sint32 v) + MEMPTR operator+(sint32 v) noexcept { // pointer arithmetic return MEMPTR(this->GetMPTR() + v * 4); } - MEMPTR operator-(sint32 v) + MEMPTR operator-(sint32 v) noexcept { // pointer arithmetic return MEMPTR(this->GetMPTR() - v * 4); } - MEMPTR& operator+=(sint32 v) + MEMPTR& operator+=(sint32 v) noexcept { m_value += v * sizeof(T); return *this; } - template - typename std::enable_if::value, Q>::type& - operator*() const { return *GetPtr(); } + template + std::enable_if_t, Q>& operator*() const noexcept + { + return *GetPtr(); + } - T* operator->() const { return GetPtr(); } + constexpr T* operator->() const noexcept + { + return GetPtr(); + } - template - typename std::enable_if::value, Q>::type& - operator[](int index) { return GetPtr()[index]; } + template + std::enable_if_t, Q>& operator[](int index) noexcept + { + return GetPtr()[index]; + } - T* GetPtr() const { return (T*)(m_value == 0 ? nullptr : memory_base + (uint32)m_value); } + T* GetPtr() const noexcept + { + return (T*)(m_value == 0 ? nullptr : memory_base + (uint32)m_value); + } - template - C* GetPtr() const { return (C*)(GetPtr()); } + template + C* GetPtr() const noexcept + { + return static_cast(GetPtr()); + } - constexpr uint32 GetMPTR() const { return m_value.value(); } - constexpr const uint32be& GetBEValue() const { return m_value; } + [[nodiscard]] constexpr uint32 GetMPTR() const noexcept + { + return m_value.value(); + } + [[nodiscard]] constexpr const uint32be& GetBEValue() const noexcept + { + return m_value; + } - constexpr bool IsNull() const { return m_value == 0; } + [[nodiscard]] constexpr bool IsNull() const noexcept + { + return m_value == 0; + } -private: + private: uint32be m_value; }; static_assert(sizeof(MEMPTR) == sizeof(uint32be)); +static_assert(std::is_trivially_copyable_v>); #include "StackAllocator.h" #include "SysAllocator.h" -template +template struct fmt::formatter> : formatter { - template - auto format(const MEMPTR& v, FormatContext& ctx) const -> format_context::iterator { return fmt::format_to(ctx.out(), "{:#x}", v.GetMPTR()); } + template + auto format(const MEMPTR& v, FormatContext& ctx) const -> format_context::iterator + { + return fmt::format_to(ctx.out(), "{:#x}", v.GetMPTR()); + } }; diff --git a/src/Common/precompiled.h b/src/Common/precompiled.h index 61707519..d4df4343 100644 --- a/src/Common/precompiled.h +++ b/src/Common/precompiled.h @@ -394,16 +394,10 @@ void vectorRemoveByIndex(std::vector& vec, const size_t index) vec.erase(vec.begin() + index); } -template -int match_any_of(T1 value, T2 compareTo) +template +bool match_any_of(T1&& value, Types&&... others) { - return value == compareTo; -} - -template -bool match_any_of(T1 value, T2 compareTo, Types&&... others) -{ - return value == compareTo || match_any_of(value, others...); + return ((value == others) || ...); } // we cache the frequency in a static variable @@ -501,13 +495,6 @@ bool future_is_ready(std::future& f) #endif } -// replace with std::scope_exit once available -struct scope_exit -{ - std::function f_; - explicit scope_exit(std::function f) noexcept : f_(std::move(f)) {} - ~scope_exit() { if (f_) f_(); } -}; // helper function to cast raw pointers to std::atomic // this is technically not legal but works on most platforms as long as alignment restrictions are met and the implementation of atomic doesnt come with additional members @@ -515,6 +502,8 @@ struct scope_exit template std::atomic* _rawPtrToAtomic(T* ptr) { + static_assert(sizeof(T) == sizeof(std::atomic)); + cemu_assert_debug((reinterpret_cast(ptr) % alignof(std::atomic)) == 0); return reinterpret_cast*>(ptr); } @@ -578,13 +567,34 @@ struct fmt::formatter> : fmt::formatter } }; -// useful C++23 stuff that isn't yet widely supported - -// std::to_underlying +// useful future C++ stuff namespace stdx { + // std::to_underlying template {} >> constexpr std::underlying_type_t to_underlying(EnumT e) noexcept { return static_cast>(e); }; + + // std::scope_exit + template + class scope_exit + { + Fn m_func; + bool m_released = false; + public: + explicit scope_exit(Fn&& f) noexcept + : m_func(std::forward(f)) + {} + ~scope_exit() + { + if (!m_released) m_func(); + } + scope_exit(scope_exit&& other) noexcept + : m_func(std::move(other.m_func)), m_released(std::exchange(other.m_released, true)) + {} + scope_exit(const scope_exit&) = delete; + scope_exit& operator=(scope_exit) = delete; + void release() { m_released = true;} + }; } \ No newline at end of file diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index f91c1e3a..50ff3b89 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -15,6 +15,9 @@ #if BOOST_OS_LINUX && HAS_WAYLAND #include "gui/helpers/wxWayland.h" #endif +#if __WXGTK__ +#include +#endif #include #include diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 5af88176..5ac5ebfd 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -50,7 +50,6 @@ add_library(CemuUtil MemMapper/MemMapper.h SystemInfo/SystemInfo.cpp SystemInfo/SystemInfo.h - ThreadPool/ThreadPool.cpp ThreadPool/ThreadPool.h tinyxml2/tinyxml2.cpp tinyxml2/tinyxml2.h diff --git a/src/util/ThreadPool/ThreadPool.cpp b/src/util/ThreadPool/ThreadPool.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/src/util/containers/robin_hood.h b/src/util/containers/robin_hood.h index 577521b1..4f76519f 100644 --- a/src/util/containers/robin_hood.h +++ b/src/util/containers/robin_hood.h @@ -194,7 +194,7 @@ namespace robin_hood { // workaround missing "is_trivially_copyable" in g++ < 5.0 // See https://stackoverflow.com/a/31798726/48181 -#if defined(__GNUC__) && __GNUC__ < 5 +#if defined(__GNUC__) && __GNUC__ < 5 && !defined(__clang__) # define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) __has_trivial_copy(__VA_ARGS__) #else # define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) std::is_trivially_copyable<__VA_ARGS__>::value diff --git a/src/util/crypto/crc32.cpp b/src/util/crypto/crc32.cpp index 52eb8a88..a5b37a5e 100644 --- a/src/util/crypto/crc32.cpp +++ b/src/util/crypto/crc32.cpp @@ -1,29 +1,6 @@ #include "crc32.h" -#if defined(_MSC_VER) || defined(__MINGW32__) -#define __LITTLE_ENDIAN 1234 -#define __BIG_ENDIAN 4321 -#define __BYTE_ORDER __LITTLE_ENDIAN - -#include -#ifdef __MINGW32__ -#define PREFETCH(location) __builtin_prefetch(location) -#else -#define PREFETCH(location) _mm_prefetch(location, _MM_HINT_T0) -#endif -#else -// defines __BYTE_ORDER as __LITTLE_ENDIAN or __BIG_ENDIAN -#include - -#ifdef __GNUC__ -#define PREFETCH(location) __builtin_prefetch(location) -#else - // no prefetching -#define PREFETCH(location) ; -#endif -#endif - -unsigned int Crc32Lookup[8][256] = +constexpr uint32 Crc32Lookup[8][256] = { { 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3, @@ -301,20 +278,7 @@ unsigned int Crc32Lookup[8][256] = } }; -/// swap endianess -static inline uint32_t swap(uint32_t x) -{ -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap32(x); -#else - return (x >> 24) | - ((x >> 8) & 0x0000FF00) | - ((x << 8) & 0x00FF0000) | - (x << 24); -#endif -} - -unsigned int crc32_calc_slice_by_8(unsigned int previousCrc32, const void* data, int length) +uint32 crc32_calc_slice_by_8(uint32 previousCrc32, const void* data, size_t length) { uint32_t crc = ~previousCrc32; // same as previousCrc32 ^ 0xFFFFFFFF const uint32_t* current = (const uint32_t*)data; @@ -323,7 +287,7 @@ unsigned int crc32_calc_slice_by_8(unsigned int previousCrc32, const void* data, while (length >= 8) { if constexpr (std::endian::native == std::endian::big){ - uint32_t one = *current++ ^ swap(crc); + uint32_t one = *current++ ^ _swapEndianU32(crc); uint32_t two = *current++; crc = Crc32Lookup[0][two & 0xFF] ^ Crc32Lookup[1][(two >> 8) & 0xFF] ^ @@ -348,13 +312,14 @@ unsigned int crc32_calc_slice_by_8(unsigned int previousCrc32, const void* data, Crc32Lookup[7][one & 0xFF]; } else { - cemu_assert(false); + static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little, + "Platform byte-order is unsupported"); } length -= 8; } - const uint8_t* currentChar = (const uint8_t*)current; + const uint8* currentChar = (const uint8*)current; // remaining 1 to 7 bytes (standard algorithm) while (length-- != 0) crc = (crc >> 8) ^ Crc32Lookup[0][(crc & 0xFF) ^ *currentChar++]; @@ -362,20 +327,20 @@ unsigned int crc32_calc_slice_by_8(unsigned int previousCrc32, const void* data, return ~crc; // same as crc ^ 0xFFFFFFFF } -unsigned int crc32_calc(unsigned int c, const void* data, int length) +uint32 crc32_calc(uint32 c, const void* data, size_t length) { if (length >= 16) { return crc32_calc_slice_by_8(c, data, length); } - unsigned char* p = (unsigned char*)data; + const uint8* p = (const uint8*)data; if (length == 0) return c; c ^= 0xFFFFFFFF; while (length) { - unsigned char temp = *p; - temp ^= (unsigned char)c; + uint8 temp = *p; + temp ^= (uint8)c; c = (c >> 8) ^ Crc32Lookup[0][temp]; // next length--; diff --git a/src/util/crypto/crc32.h b/src/util/crypto/crc32.h index b8002261..2ab37376 100644 --- a/src/util/crypto/crc32.h +++ b/src/util/crypto/crc32.h @@ -1,8 +1,8 @@ #pragma once -unsigned int crc32_calc(unsigned int c, const void* data, int length); +uint32 crc32_calc(uint32 c, const void* data, size_t length); -inline unsigned int crc32_calc(const void* data, int length) +inline uint32 crc32_calc(const void* data, size_t length) { return crc32_calc(0, data, length); } \ No newline at end of file From 6dc73f5d797082c25a68ad162377077547948d26 Mon Sep 17 00:00:00 2001 From: Alexandre Bouvier Date: Thu, 3 Oct 2024 06:48:25 +0000 Subject: [PATCH 024/137] Add support for fmt 11 (#1366) --- src/Cemu/Logging/CemuLogging.h | 4 ++++ src/config/CemuConfig.h | 10 +++++----- src/gui/helpers/wxHelpers.h | 2 +- src/input/emulated/EmulatedController.h | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Cemu/Logging/CemuLogging.h b/src/Cemu/Logging/CemuLogging.h index a671ce51..5b2e5fa4 100644 --- a/src/Cemu/Logging/CemuLogging.h +++ b/src/Cemu/Logging/CemuLogging.h @@ -91,7 +91,11 @@ bool cemuLog_log(LogType type, std::basic_string formatStr, TArgs&&... args) else { const auto format_view = fmt::basic_string_view(formatStr); +#if FMT_VERSION >= 110000 + const auto text = fmt::vformat(format_view, fmt::make_format_args>(args...)); +#else const auto text = fmt::vformat(format_view, fmt::make_format_args>(args...)); +#endif cemuLog_log(type, std::basic_string_view(text.data(), text.size())); } return true; diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index e2fbb74c..2f22cd76 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -194,7 +194,7 @@ ENABLE_ENUM_ITERATORS(CrashDump, CrashDump::Disabled, CrashDump::Enabled); template <> struct fmt::formatter : formatter { template - auto format(const PrecompiledShaderOption c, FormatContext &ctx) { + auto format(const PrecompiledShaderOption c, FormatContext &ctx) const { string_view name; switch (c) { @@ -209,7 +209,7 @@ struct fmt::formatter : formatter { template <> struct fmt::formatter : formatter { template - auto format(const AccurateShaderMulOption c, FormatContext &ctx) { + auto format(const AccurateShaderMulOption c, FormatContext &ctx) const { string_view name; switch (c) { @@ -223,7 +223,7 @@ struct fmt::formatter : formatter { template <> struct fmt::formatter : formatter { template - auto format(const CPUMode c, FormatContext &ctx) { + auto format(const CPUMode c, FormatContext &ctx) const { string_view name; switch (c) { @@ -240,7 +240,7 @@ struct fmt::formatter : formatter { template <> struct fmt::formatter : formatter { template - auto format(const CPUModeLegacy c, FormatContext &ctx) { + auto format(const CPUModeLegacy c, FormatContext &ctx) const { string_view name; switch (c) { @@ -257,7 +257,7 @@ struct fmt::formatter : formatter { template <> struct fmt::formatter : formatter { template - auto format(const CafeConsoleRegion v, FormatContext &ctx) { + auto format(const CafeConsoleRegion v, FormatContext &ctx) const { string_view name; switch (v) { diff --git a/src/gui/helpers/wxHelpers.h b/src/gui/helpers/wxHelpers.h index fa135cf4..9e00bf48 100644 --- a/src/gui/helpers/wxHelpers.h +++ b/src/gui/helpers/wxHelpers.h @@ -8,7 +8,7 @@ template <> struct fmt::formatter : formatter { template - auto format(const wxString& str, FormatContext& ctx) + auto format(const wxString& str, FormatContext& ctx) const { return formatter::format(str.c_str().AsChar(), ctx); } diff --git a/src/input/emulated/EmulatedController.h b/src/input/emulated/EmulatedController.h index 907be07e..c5adf81e 100644 --- a/src/input/emulated/EmulatedController.h +++ b/src/input/emulated/EmulatedController.h @@ -127,7 +127,7 @@ using EmulatedControllerPtr = std::shared_ptr; template <> struct fmt::formatter : formatter { template - auto format(EmulatedController::Type v, FormatContext& ctx) { + auto format(EmulatedController::Type v, FormatContext& ctx) const { switch (v) { case EmulatedController::Type::VPAD: return formatter::format("Wii U Gamepad", ctx); From 3acd0c4f2ca328abe80f2d35fb20136e738c84ae Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:03:36 +0200 Subject: [PATCH 025/137] Vulkan: Protect against uniform var ringbuffer overflow (#1378) --- .../Latte/Renderer/Vulkan/VulkanRenderer.cpp | 8 +- .../HW/Latte/Renderer/Vulkan/VulkanRenderer.h | 4 +- .../Renderer/Vulkan/VulkanRendererCore.cpp | 99 +++++++++++++------ 3 files changed, 75 insertions(+), 36 deletions(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 12d1d975..9ad2c5ca 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -1892,6 +1892,7 @@ void VulkanRenderer::ProcessFinishedCommandBuffers() if (fenceStatus == VK_SUCCESS) { ProcessDestructionQueue(); + m_uniformVarBufferReadIndex = m_cmdBufferUniformRingbufIndices[m_commandBufferSyncIndex]; m_commandBufferSyncIndex = (m_commandBufferSyncIndex + 1) % m_commandBuffers.size(); memoryManager->cleanupBuffers(m_countCommandBufferFinished); m_countCommandBufferFinished++; @@ -1985,6 +1986,7 @@ void VulkanRenderer::SubmitCommandBuffer(VkSemaphore signalSemaphore, VkSemaphor cemuLog_logDebug(LogType::Force, "Vulkan: Waiting for available command buffer..."); WaitForNextFinishedCommandBuffer(); } + m_cmdBufferUniformRingbufIndices[nextCmdBufferIndex] = m_cmdBufferUniformRingbufIndices[m_commandBufferIndex]; m_commandBufferIndex = nextCmdBufferIndex; @@ -3562,13 +3564,13 @@ void VulkanRenderer::buffer_bindUniformBuffer(LatteConst::ShaderType shaderType, switch (shaderType) { case LatteConst::ShaderType::Vertex: - dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_VERTEX].unformBufferOffset[bufferIndex] = offset; + dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_VERTEX].uniformBufferOffset[bufferIndex] = offset; break; case LatteConst::ShaderType::Geometry: - dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_GEOMETRY].unformBufferOffset[bufferIndex] = offset; + dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_GEOMETRY].uniformBufferOffset[bufferIndex] = offset; break; case LatteConst::ShaderType::Pixel: - dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_FRAGMENT].unformBufferOffset[bufferIndex] = offset; + dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_FRAGMENT].uniformBufferOffset[bufferIndex] = offset; break; default: cemu_assert_debug(false); diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h index ce97b5e9..52c1c6ed 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h @@ -591,6 +591,7 @@ private: bool m_uniformVarBufferMemoryIsCoherent{false}; uint8* m_uniformVarBufferPtr = nullptr; uint32 m_uniformVarBufferWriteIndex = 0; + uint32 m_uniformVarBufferReadIndex = 0; // transform feedback ringbuffer VkBuffer m_xfbRingBuffer = VK_NULL_HANDLE; @@ -637,6 +638,7 @@ private: size_t m_commandBufferIndex = 0; // current buffer being filled size_t m_commandBufferSyncIndex = 0; // latest buffer that finished execution (updated on submit) size_t m_commandBufferIDOfPrevFrame = 0; + std::array m_cmdBufferUniformRingbufIndices {}; // index in the uniform ringbuffer std::array m_cmd_buffer_fences; std::array m_commandBuffers; std::array m_commandBufferSemaphores; @@ -659,7 +661,7 @@ private: uint32 uniformVarBufferOffset[VulkanRendererConst::SHADER_STAGE_INDEX_COUNT]; struct { - uint32 unformBufferOffset[LATTE_NUM_MAX_UNIFORM_BUFFERS]; + uint32 uniformBufferOffset[LATTE_NUM_MAX_UNIFORM_BUFFERS]; }shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_COUNT]; }dynamicOffsetInfo{}; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp index 6500f7d3..3a684072 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp @@ -375,24 +375,20 @@ float s_vkUniformData[512 * 4]; void VulkanRenderer::uniformData_updateUniformVars(uint32 shaderStageIndex, LatteDecompilerShader* shader) { - auto GET_UNIFORM_DATA_PTR = [&](size_t index) { return s_vkUniformData + (index / 4); }; + auto GET_UNIFORM_DATA_PTR = [](size_t index) { return s_vkUniformData + (index / 4); }; sint32 shaderAluConst; - sint32 shaderUniformRegisterOffset; switch (shader->shaderType) { case LatteConst::ShaderType::Vertex: shaderAluConst = 0x400; - shaderUniformRegisterOffset = mmSQ_VTX_UNIFORM_BLOCK_START; break; case LatteConst::ShaderType::Pixel: shaderAluConst = 0; - shaderUniformRegisterOffset = mmSQ_PS_UNIFORM_BLOCK_START; break; case LatteConst::ShaderType::Geometry: shaderAluConst = 0; // geometry shader has no ALU const - shaderUniformRegisterOffset = mmSQ_GS_UNIFORM_BLOCK_START; break; default: UNREACHABLE; @@ -445,7 +441,7 @@ void VulkanRenderer::uniformData_updateUniformVars(uint32 shaderStageIndex, Latt } if (shader->uniform.loc_verticesPerInstance >= 0) { - *(int*)(s_vkUniformData + ((size_t)shader->uniform.loc_verticesPerInstance / 4)) = m_streamoutState.verticesPerInstance; + *(int*)GET_UNIFORM_DATA_PTR(shader->uniform.loc_verticesPerInstance) = m_streamoutState.verticesPerInstance; for (sint32 b = 0; b < LATTE_NUM_STREAMOUT_BUFFER; b++) { if (shader->uniform.loc_streamoutBufferBase[b] >= 0) @@ -455,26 +451,63 @@ void VulkanRenderer::uniformData_updateUniformVars(uint32 shaderStageIndex, Latt } } // upload - if ((m_uniformVarBufferWriteIndex + shader->uniform.uniformRangeSize + 1024) > UNIFORMVAR_RINGBUFFER_SIZE) + const uint32 bufferAlignmentM1 = std::max(m_featureControl.limits.minUniformBufferOffsetAlignment, m_featureControl.limits.nonCoherentAtomSize) - 1; + const uint32 uniformSize = (shader->uniform.uniformRangeSize + bufferAlignmentM1) & ~bufferAlignmentM1; + + auto waitWhileCondition = [&](std::function condition) { + while (condition()) + { + if (m_commandBufferSyncIndex == m_commandBufferIndex) + { + if (m_cmdBufferUniformRingbufIndices[m_commandBufferIndex] != m_uniformVarBufferReadIndex) + { + draw_endRenderPass(); + SubmitCommandBuffer(); + } + else + { + // submitting work would not change readIndex, so there's no way for conditions based on it to change + cemuLog_log(LogType::Force, "draw call overflowed and corrupted uniform ringbuffer. expect visual corruption"); + cemu_assert_suspicious(); + break; + } + } + WaitForNextFinishedCommandBuffer(); + } + }; + + // wrap around if it doesnt fit consecutively + if (m_uniformVarBufferWriteIndex + uniformSize > UNIFORMVAR_RINGBUFFER_SIZE) { + waitWhileCondition([&]() { + return m_uniformVarBufferReadIndex > m_uniformVarBufferWriteIndex || m_uniformVarBufferReadIndex == 0; + }); m_uniformVarBufferWriteIndex = 0; } - uint32 bufferAlignmentM1 = std::max(m_featureControl.limits.minUniformBufferOffsetAlignment, m_featureControl.limits.nonCoherentAtomSize) - 1; + + auto ringBufRemaining = [&]() { + ssize_t ringBufferUsedBytes = (ssize_t)m_uniformVarBufferWriteIndex - m_uniformVarBufferReadIndex; + if (ringBufferUsedBytes < 0) + ringBufferUsedBytes += UNIFORMVAR_RINGBUFFER_SIZE; + return UNIFORMVAR_RINGBUFFER_SIZE - 1 - ringBufferUsedBytes; + }; + waitWhileCondition([&]() { + return ringBufRemaining() < uniformSize; + }); + const uint32 uniformOffset = m_uniformVarBufferWriteIndex; memcpy(m_uniformVarBufferPtr + uniformOffset, s_vkUniformData, shader->uniform.uniformRangeSize); - m_uniformVarBufferWriteIndex += shader->uniform.uniformRangeSize; - m_uniformVarBufferWriteIndex = (m_uniformVarBufferWriteIndex + bufferAlignmentM1) & ~bufferAlignmentM1; + m_uniformVarBufferWriteIndex += uniformSize; // update dynamic offset dynamicOffsetInfo.uniformVarBufferOffset[shaderStageIndex] = uniformOffset; // flush if not coherent if (!m_uniformVarBufferMemoryIsCoherent) { - uint32 nonCoherentAtomSizeM1 = m_featureControl.limits.nonCoherentAtomSize - 1; VkMappedMemoryRange flushedRange{}; flushedRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; flushedRange.memory = m_uniformVarBufferMemory; flushedRange.offset = uniformOffset; - flushedRange.size = (shader->uniform.uniformRangeSize + nonCoherentAtomSizeM1) & ~nonCoherentAtomSizeM1; + flushedRange.size = uniformSize; vkFlushMappedMemoryRanges(m_logicalDevice, 1, &flushedRange); } } @@ -494,7 +527,7 @@ void VulkanRenderer::draw_prepareDynamicOffsetsForDescriptorSet(uint32 shaderSta { for (auto& itr : pipeline_info->dynamicOffsetInfo.list_uniformBuffers[shaderStageIndex]) { - dynamicOffsets[numDynOffsets] = dynamicOffsetInfo.shaderUB[shaderStageIndex].unformBufferOffset[itr]; + dynamicOffsets[numDynOffsets] = dynamicOffsetInfo.shaderUB[shaderStageIndex].uniformBufferOffset[itr]; numDynOffsets++; } } @@ -1357,6 +1390,24 @@ void VulkanRenderer::draw_execute(uint32 baseVertex, uint32 baseInstance, uint32 return; } + // prepare streamout + m_streamoutState.verticesPerInstance = count; + LatteStreamout_PrepareDrawcall(count, instanceCount); + + // update uniform vars + LatteDecompilerShader* vertexShader = LatteSHRC_GetActiveVertexShader(); + LatteDecompilerShader* pixelShader = LatteSHRC_GetActivePixelShader(); + LatteDecompilerShader* geometryShader = LatteSHRC_GetActiveGeometryShader(); + + if (vertexShader) + uniformData_updateUniformVars(VulkanRendererConst::SHADER_STAGE_INDEX_VERTEX, vertexShader); + if (pixelShader) + uniformData_updateUniformVars(VulkanRendererConst::SHADER_STAGE_INDEX_FRAGMENT, pixelShader); + if (geometryShader) + uniformData_updateUniformVars(VulkanRendererConst::SHADER_STAGE_INDEX_GEOMETRY, geometryShader); + // store where the read pointer should go after command buffer execution + m_cmdBufferUniformRingbufIndices[m_commandBufferIndex] = m_uniformVarBufferWriteIndex; + // process index data const LattePrimitiveMode primitiveMode = static_cast(LatteGPUState.contextRegister[mmVGT_PRIMITIVE_TYPE]); @@ -1410,22 +1461,6 @@ void VulkanRenderer::draw_execute(uint32 baseVertex, uint32 baseInstance, uint32 LatteBufferCache_Sync(indexMin + baseVertex, indexMax + baseVertex, baseInstance, instanceCount); } - // prepare streamout - m_streamoutState.verticesPerInstance = count; - LatteStreamout_PrepareDrawcall(count, instanceCount); - - // update uniform vars - LatteDecompilerShader* vertexShader = LatteSHRC_GetActiveVertexShader(); - LatteDecompilerShader* pixelShader = LatteSHRC_GetActivePixelShader(); - LatteDecompilerShader* geometryShader = LatteSHRC_GetActiveGeometryShader(); - - if (vertexShader) - uniformData_updateUniformVars(VulkanRendererConst::SHADER_STAGE_INDEX_VERTEX, vertexShader); - if (pixelShader) - uniformData_updateUniformVars(VulkanRendererConst::SHADER_STAGE_INDEX_FRAGMENT, pixelShader); - if (geometryShader) - uniformData_updateUniformVars(VulkanRendererConst::SHADER_STAGE_INDEX_GEOMETRY, geometryShader); - PipelineInfo* pipeline_info; if (!isFirst) @@ -1613,13 +1648,13 @@ void VulkanRenderer::draw_updateUniformBuffersDirectAccess(LatteDecompilerShader switch (shaderType) { case LatteConst::ShaderType::Vertex: - dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_VERTEX].unformBufferOffset[bufferIndex] = physicalAddr - m_importedMemBaseAddress; + dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_VERTEX].uniformBufferOffset[bufferIndex] = physicalAddr - m_importedMemBaseAddress; break; case LatteConst::ShaderType::Geometry: - dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_GEOMETRY].unformBufferOffset[bufferIndex] = physicalAddr - m_importedMemBaseAddress; + dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_GEOMETRY].uniformBufferOffset[bufferIndex] = physicalAddr - m_importedMemBaseAddress; break; case LatteConst::ShaderType::Pixel: - dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_FRAGMENT].unformBufferOffset[bufferIndex] = physicalAddr - m_importedMemBaseAddress; + dynamicOffsetInfo.shaderUB[VulkanRendererConst::SHADER_STAGE_INDEX_FRAGMENT].uniformBufferOffset[bufferIndex] = physicalAddr - m_importedMemBaseAddress; break; default: UNREACHABLE; From d6575455eedf8fc4dbe4bd1b8101361f4257d0c4 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:24:01 +0200 Subject: [PATCH 026/137] Linux: Fix crash on invalid command-line arguments use std::cout instead of wxMessageBox which does not work when wxWidgets has not been initialised yet --- src/config/LaunchSettings.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config/LaunchSettings.cpp b/src/config/LaunchSettings.cpp index bf38b9cf..32a069c6 100644 --- a/src/config/LaunchSettings.cpp +++ b/src/config/LaunchSettings.cpp @@ -199,7 +199,11 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) std::string errorMsg; errorMsg.append("Error while trying to parse command line parameter:\n"); errorMsg.append(ex.what()); +#if BOOST_OS_WINDOWS wxMessageBox(errorMsg, "Parameter error", wxICON_ERROR); +#else + std::cout << errorMsg << std::endl; +#endif return false; } From f9a4b2dbb1ae8de07223a3d988b1605b941f35a8 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:56:56 +0200 Subject: [PATCH 027/137] input: Add option to make `show screen` button a toggle (#1383) --- src/Cafe/CafeSystem.cpp | 2 +- src/Cafe/HW/Latte/Core/Latte.h | 2 +- src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp | 47 ++++++++------------ src/gui/input/panels/VPADInputPanel.cpp | 24 +++++++++- src/gui/input/panels/VPADInputPanel.h | 3 ++ src/input/emulated/VPADController.cpp | 11 +++++ src/input/emulated/VPADController.h | 6 +++ 7 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/Cafe/CafeSystem.cpp b/src/Cafe/CafeSystem.cpp index 958a5a57..51de3550 100644 --- a/src/Cafe/CafeSystem.cpp +++ b/src/Cafe/CafeSystem.cpp @@ -396,7 +396,7 @@ void cemu_initForGame() // replace any known function signatures with our HLE implementations and patch bugs in the games GamePatch_scan(); } - LatteGPUState.alwaysDisplayDRC = ActiveSettings::DisplayDRCEnabled(); + LatteGPUState.isDRCPrimary = ActiveSettings::DisplayDRCEnabled(); InfoLog_PrintActiveSettings(); Latte_Start(); // check for debugger entrypoint bp diff --git a/src/Cafe/HW/Latte/Core/Latte.h b/src/Cafe/HW/Latte/Core/Latte.h index e8cb2be4..e5e9dd5c 100644 --- a/src/Cafe/HW/Latte/Core/Latte.h +++ b/src/Cafe/HW/Latte/Core/Latte.h @@ -52,7 +52,7 @@ struct LatteGPUState_t uint32 gx2InitCalled; // incremented every time GX2Init() is called // OpenGL control uint32 glVendor; // GLVENDOR_* - bool alwaysDisplayDRC = false; + bool isDRCPrimary = false; // temporary (replace with proper solution later) bool tvBufferUsesSRGB; bool drcBufferUsesSRGB; diff --git a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp index 60124c02..ca6a2a4d 100644 --- a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp +++ b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp @@ -989,8 +989,6 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa g_renderer->ImguiEnd(); } -bool ctrlTabHotkeyPressed = false; - void LatteRenderTarget_itHLECopyColorBufferToScanBuffer(MPTR colorBufferPtr, uint32 colorBufferWidth, uint32 colorBufferHeight, uint32 colorBufferSliceIndex, uint32 colorBufferFormat, uint32 colorBufferPitch, Latte::E_HWTILEMODE colorBufferTilemode, uint32 colorBufferSwizzle, uint32 renderTarget) { cemu_assert_debug(colorBufferSliceIndex == 0); // todo - support for non-zero slice @@ -1000,38 +998,31 @@ void LatteRenderTarget_itHLECopyColorBufferToScanBuffer(MPTR colorBufferPtr, uin return; } + auto getVPADScreenActive = [](size_t n) -> std::pair { + auto controller = InputManager::instance().get_vpad_controller(n); + if (!controller) + return {false,false}; + auto pressed = controller->is_screen_active(); + auto toggle = controller->is_screen_active_toggle(); + return {pressed && !toggle, pressed && toggle}; + }; + const bool tabPressed = gui_isKeyDown(PlatformKeyCodes::TAB); const bool ctrlPressed = gui_isKeyDown(PlatformKeyCodes::LCONTROL); + const auto [vpad0Active, vpad0Toggle] = getVPADScreenActive(0); + const auto [vpad1Active, vpad1Toggle] = getVPADScreenActive(1); - bool showDRC = swkbd_hasKeyboardInputHook() == false && tabPressed; - bool& alwaysDisplayDRC = LatteGPUState.alwaysDisplayDRC; + const bool altScreenRequested = (!ctrlPressed && tabPressed) || vpad0Active || vpad1Active; + const bool togglePressed = (ctrlPressed && tabPressed) || vpad0Toggle || vpad1Toggle; + static bool togglePressedLast = false; - if (ctrlPressed && tabPressed) - { - if (ctrlTabHotkeyPressed == false) - { - alwaysDisplayDRC = !alwaysDisplayDRC; - ctrlTabHotkeyPressed = true; - } - } - else - ctrlTabHotkeyPressed = false; + bool& isDRCPrimary = LatteGPUState.isDRCPrimary; - if (alwaysDisplayDRC) - showDRC = !tabPressed; + if(togglePressed && !togglePressedLast) + isDRCPrimary = !isDRCPrimary; + togglePressedLast = togglePressed; - if (!showDRC) - { - auto controller = InputManager::instance().get_vpad_controller(0); - if (controller && controller->is_screen_active()) - showDRC = true; - if (!showDRC) - { - controller = InputManager::instance().get_vpad_controller(1); - if (controller && controller->is_screen_active()) - showDRC = true; - } - } + bool showDRC = swkbd_hasKeyboardInputHook() == false && (isDRCPrimary ^ altScreenRequested); if ((renderTarget & RENDER_TARGET_DRC) && g_renderer->IsPadWindowActive()) LatteRenderTarget_copyToBackbuffer(texView, true); diff --git a/src/gui/input/panels/VPADInputPanel.cpp b/src/gui/input/panels/VPADInputPanel.cpp index fbcdfde4..9e6d75d6 100644 --- a/src/gui/input/panels/VPADInputPanel.cpp +++ b/src/gui/input/panels/VPADInputPanel.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "gui/helpers/wxControlObject.h" @@ -131,11 +132,23 @@ VPADInputPanel::VPADInputPanel(wxWindow* parent) } // Blow Mic - row = 9; + row = 8; add_button_row(main_sizer, row, column, VPADController::kButtonId_Mic, _("blow mic")); row++; add_button_row(main_sizer, row, column, VPADController::kButtonId_Screen, _("show screen")); + row++; + + auto toggleScreenText = new wxStaticText(this, wxID_ANY, _("toggle screen")); + main_sizer->Add(toggleScreenText, + wxGBPosition(row, column), + wxDefaultSpan, + wxALL | wxALIGN_CENTER_VERTICAL, 5); + m_togglePadViewCheckBox = new wxCheckBox(this, wxID_ANY, {}, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + wxString toggleScreenTT = _("Makes the \"show screen\" button toggle between the TV and gamepad screens"); + m_togglePadViewCheckBox->SetToolTip(toggleScreenTT); + toggleScreenText->SetToolTip(toggleScreenTT); + main_sizer->Add(m_togglePadViewCheckBox, wxGBPosition(row,column+1), wxDefaultSpan, wxALL | wxEXPAND, 5); ////////////////////////////////////////////////////////////////// @@ -168,6 +181,8 @@ void VPADInputPanel::on_timer(const EmulatedControllerPtr& emulated_controller, { InputPanel::on_timer(emulated_controller, controller_base); + static_cast(emulated_controller.get())->set_screen_toggle(m_togglePadViewCheckBox->GetValue()); + if(emulated_controller) { const auto axis = emulated_controller->get_axis(); @@ -182,3 +197,10 @@ void VPADInputPanel::OnVolumeChange(wxCommandEvent& event) { } +void VPADInputPanel::load_controller(const EmulatedControllerPtr& controller) +{ + InputPanel::load_controller(controller); + + const bool isToggle = static_cast(controller.get())->is_screen_active_toggle(); + m_togglePadViewCheckBox->SetValue(isToggle); +} diff --git a/src/gui/input/panels/VPADInputPanel.h b/src/gui/input/panels/VPADInputPanel.h index 3513bbf7..477385f4 100644 --- a/src/gui/input/panels/VPADInputPanel.h +++ b/src/gui/input/panels/VPADInputPanel.h @@ -4,6 +4,7 @@ #include "gui/input/panels/InputPanel.h" class wxInputDraw; +class wxCheckBox; class VPADInputPanel : public InputPanel { @@ -11,11 +12,13 @@ public: VPADInputPanel(wxWindow* parent); void on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) override; + virtual void load_controller(const EmulatedControllerPtr& controller) override; private: void OnVolumeChange(wxCommandEvent& event); wxInputDraw* m_left_draw, * m_right_draw; + wxCheckBox* m_togglePadViewCheckBox; void add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const VPADController::ButtonId &button_id); void add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const VPADController::ButtonId &button_id, const wxString &label); diff --git a/src/input/emulated/VPADController.cpp b/src/input/emulated/VPADController.cpp index aeb56395..f1ab1bc4 100644 --- a/src/input/emulated/VPADController.cpp +++ b/src/input/emulated/VPADController.cpp @@ -686,3 +686,14 @@ bool VPADController::set_default_mapping(const std::shared_ptr& return mapping_updated; } + +void VPADController::load(const pugi::xml_node& node) +{ + if (const auto value = node.child("toggle_display")) + m_screen_active_toggle = ConvertString(value.child_value()); +} + +void VPADController::save(pugi::xml_node& node) +{ + node.append_child("toggle_display").append_child(pugi::node_pcdata).set_value(fmt::format("{}", (int)m_screen_active_toggle).c_str()); +} diff --git a/src/input/emulated/VPADController.h b/src/input/emulated/VPADController.h index 6aef16ae..937062da 100644 --- a/src/input/emulated/VPADController.h +++ b/src/input/emulated/VPADController.h @@ -66,6 +66,8 @@ public: bool is_mic_active() { return m_mic_active; } bool is_screen_active() { return m_screen_active; } + bool is_screen_active_toggle() { return m_screen_active_toggle; } + void set_screen_toggle(bool toggle) {m_screen_active_toggle = toggle;} static std::string_view get_button_name(ButtonId id); @@ -86,9 +88,13 @@ public: bool set_default_mapping(const std::shared_ptr& controller) override; + void load(const pugi::xml_node& node) override; + void save(pugi::xml_node& node) override; + private: bool m_mic_active = false; bool m_screen_active = false; + bool m_screen_active_toggle = false; uint32be m_last_holdvalue = 0; std::chrono::high_resolution_clock::time_point m_last_hold_change{}, m_last_pulse{}; From 63e1289bb518562ae62033389610a1e772e63c8b Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Fri, 25 Oct 2024 17:48:21 +0100 Subject: [PATCH 028/137] Windows: Save icons to Cemu user data directory (#1390) --- src/gui/components/wxGameList.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/gui/components/wxGameList.cpp b/src/gui/components/wxGameList.cpp index eedfde5d..6cbb5859 100644 --- a/src/gui/components/wxGameList.cpp +++ b/src/gui/components/wxGameList.cpp @@ -1392,7 +1392,6 @@ void wxGameList::CreateShortcut(GameInfo2& gameInfo) const auto outputPath = shortcutDialog.GetPath(); std::optional icon_path = std::nullopt; - [&]() { int iconIdx; int smallIconIdx; @@ -1402,15 +1401,13 @@ void wxGameList::CreateShortcut(GameInfo2& gameInfo) return; } const auto icon = m_image_list->GetIcon(iconIdx); - PWSTR localAppData; - const auto hres = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &localAppData); - wxBitmap bitmap{}; - auto folder = fs::path(localAppData) / "Cemu" / "icons"; - if (!SUCCEEDED(hres) || (!fs::exists(folder) && !fs::create_directories(folder))) + const auto folder = ActiveSettings::GetUserDataPath("icons"); + if (!fs::exists(folder) && !fs::create_directories(folder)) { cemuLog_log(LogType::Force, "Failed to create icon directory"); return; } + wxBitmap bitmap{}; if (!bitmap.CopyFromIcon(icon)) { cemuLog_log(LogType::Force, "Failed to copy icon"); @@ -1426,7 +1423,7 @@ void wxGameList::CreateShortcut(GameInfo2& gameInfo) icon_path = std::nullopt; cemuLog_log(LogType::Force, "Icon failed to save"); } - }(); + } IShellLinkW* shellLink; HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, reinterpret_cast(&shellLink)); From 459fd5d9bb4897381c4d7d326302ca707c7b818c Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:37:30 +0100 Subject: [PATCH 029/137] input: Fix crash when closing add controller dialog before search completes (#1386) --- src/gui/input/InputAPIAddWindow.cpp | 43 ++++++++++++++++++++++------- src/gui/input/InputAPIAddWindow.h | 9 ++++++ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/gui/input/InputAPIAddWindow.cpp b/src/gui/input/InputAPIAddWindow.cpp index a6d1f1a9..688ee14e 100644 --- a/src/gui/input/InputAPIAddWindow.cpp +++ b/src/gui/input/InputAPIAddWindow.cpp @@ -114,6 +114,11 @@ InputAPIAddWindow::InputAPIAddWindow(wxWindow* parent, const wxPoint& position, this->Bind(wxControllersRefreshed, &InputAPIAddWindow::on_controllers_refreshed, this); } +InputAPIAddWindow::~InputAPIAddWindow() +{ + discard_thread_result(); +} + void InputAPIAddWindow::on_add_button(wxCommandEvent& event) { const auto selection = m_input_api->GetSelection(); @@ -159,6 +164,8 @@ std::unique_ptr InputAPIAddWindow::get_settings() co void InputAPIAddWindow::on_api_selected(wxCommandEvent& event) { + discard_thread_result(); + if (m_input_api->GetSelection() == wxNOT_FOUND) return; @@ -239,19 +246,25 @@ void InputAPIAddWindow::on_controller_dropdown(wxCommandEvent& event) m_controller_list->Append(_("Searching for controllers..."), (wxClientData*)nullptr); m_controller_list->SetSelection(wxNOT_FOUND); - std::thread([this, provider, selected_uuid]() + m_search_thread_data = std::make_unique(); + std::thread([this, provider, selected_uuid](std::shared_ptr data) { auto available_controllers = provider->get_controllers(); - wxCommandEvent event(wxControllersRefreshed); - event.SetEventObject(m_controller_list); - event.SetClientObject(new wxCustomData(std::move(available_controllers))); - event.SetInt(provider->api()); - event.SetString(selected_uuid); - wxPostEvent(this, event); - - m_search_running = false; - }).detach(); + { + std::lock_guard lock{data->mutex}; + if(!data->discardResult) + { + wxCommandEvent event(wxControllersRefreshed); + event.SetEventObject(m_controller_list); + event.SetClientObject(new wxCustomData(std::move(available_controllers))); + event.SetInt(provider->api()); + event.SetString(selected_uuid); + wxPostEvent(this, event); + m_search_running = false; + } + } + }, m_search_thread_data).detach(); } void InputAPIAddWindow::on_controller_selected(wxCommandEvent& event) @@ -301,3 +314,13 @@ void InputAPIAddWindow::on_controllers_refreshed(wxCommandEvent& event) } } } + +void InputAPIAddWindow::discard_thread_result() +{ + m_search_running = false; + if(m_search_thread_data) + { + std::lock_guard lock{m_search_thread_data->mutex}; + m_search_thread_data->discardResult = true; + } +} diff --git a/src/gui/input/InputAPIAddWindow.h b/src/gui/input/InputAPIAddWindow.h index ebee0592..085dd623 100644 --- a/src/gui/input/InputAPIAddWindow.h +++ b/src/gui/input/InputAPIAddWindow.h @@ -19,6 +19,7 @@ class InputAPIAddWindow : public wxDialog { public: InputAPIAddWindow(wxWindow* parent, const wxPoint& position, const std::vector& controllers); + ~InputAPIAddWindow(); bool is_valid() const { return m_type.has_value() && m_controller != nullptr; } InputAPI::Type get_type() const { return m_type.value(); } @@ -38,6 +39,8 @@ private: void on_controller_selected(wxCommandEvent& event); void on_controllers_refreshed(wxCommandEvent& event); + void discard_thread_result(); + wxChoice* m_input_api; wxComboBox* m_controller_list; wxButton* m_ok_button; @@ -50,4 +53,10 @@ private: std::vector m_controllers; std::atomic_bool m_search_running = false; + struct AsyncThreadData + { + std::atomic_bool discardResult = false; + std::mutex mutex; + }; + std::shared_ptr m_search_thread_data; }; From 47001ad23349fae130d6faae9409117899eb11cc Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 30 Oct 2024 22:10:32 +0000 Subject: [PATCH 030/137] Make `MEMPTR` a little more `T*`-like (#1385) --- src/Common/MemPtr.h | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Common/MemPtr.h b/src/Common/MemPtr.h index 7825e4d5..2dc92040 100644 --- a/src/Common/MemPtr.h +++ b/src/Common/MemPtr.h @@ -98,35 +98,36 @@ class MEMPTR : MEMPTRBase return MEMPTR(this->m_value); } - MEMPTR operator+(const MEMPTR& ptr) noexcept + sint32 operator-(const MEMPTR& ptr) noexcept + requires(!std::is_void_v) { - return MEMPTR(this->GetMPTR() + ptr.GetMPTR()); - } - MEMPTR operator-(const MEMPTR& ptr) noexcept - { - return MEMPTR(this->GetMPTR() - ptr.GetMPTR()); + return static_cast(this->GetMPTR() - ptr.GetMPTR()); } MEMPTR operator+(sint32 v) noexcept + requires(!std::is_void_v) { // pointer arithmetic - return MEMPTR(this->GetMPTR() + v * 4); + return MEMPTR(this->GetMPTR() + v * sizeof(T)); } MEMPTR operator-(sint32 v) noexcept + requires(!std::is_void_v) { // pointer arithmetic - return MEMPTR(this->GetMPTR() - v * 4); + return MEMPTR(this->GetMPTR() - v * sizeof(T)); } MEMPTR& operator+=(sint32 v) noexcept + requires(!std::is_void_v) { m_value += v * sizeof(T); return *this; } template - std::enable_if_t, Q>& operator*() const noexcept + requires(!std::is_void_v) + Q& operator*() const noexcept { return *GetPtr(); } @@ -137,7 +138,8 @@ class MEMPTR : MEMPTRBase } template - std::enable_if_t, Q>& operator[](int index) noexcept + requires(!std::is_void_v) + Q& operator[](int index) noexcept { return GetPtr()[index]; } From 1c49a8a1ba8fea6e656c1d535f35e316bc29da76 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:47:19 +0100 Subject: [PATCH 031/137] nn_nfp: Implement GetNfpReadOnlyInfo and fix deactivate event Fixes Amiibos not being detected in MK8 --- src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp | 102 +++++++++++++++++------------ 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp b/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp index 10d9e7cb..38e0c4fe 100644 --- a/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp +++ b/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp @@ -334,45 +334,63 @@ void nnNfpExport_MountRom(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_NFP, 0)); } -typedef struct +namespace nn::nfp { - /* +0x00 */ uint8 characterId[3]; - /* +0x03 */ uint8 amiiboSeries; - /* +0x04 */ uint16be number; - /* +0x06 */ uint8 nfpType; - /* +0x07 */ uint8 unused[0x2F]; -}nfpRomInfo_t; - -static_assert(offsetof(nfpRomInfo_t, amiiboSeries) == 0x3, "nfpRomInfo.seriesId has invalid offset"); -static_assert(offsetof(nfpRomInfo_t, number) == 0x4, "nfpRomInfo.number has invalid offset"); -static_assert(offsetof(nfpRomInfo_t, nfpType) == 0x6, "nfpRomInfo.nfpType has invalid offset"); -static_assert(sizeof(nfpRomInfo_t) == 0x36, "nfpRomInfo_t has invalid size"); - -void nnNfpExport_GetNfpRomInfo(PPCInterpreter_t* hCPU) -{ - cemuLog_log(LogType::NN_NFP, "GetNfpRomInfo(0x{:08x})", hCPU->gpr[3]); - ppcDefineParamStructPtr(romInfo, nfpRomInfo_t, 0); - - nnNfpLock(); - if (nfp_data.hasActiveAmiibo == false) + struct RomInfo { - nnNfpUnlock(); - osLib_returnFromFunction(hCPU, BUILD_NN_RESULT(NN_RESULT_LEVEL_STATUS, NN_RESULT_MODULE_NN_NFP, 0)); // todo: Return correct error code - return; + /* +0x00 */ uint8 characterId[3]; + /* +0x03 */ uint8 amiiboSeries; + /* +0x04 */ uint16be number; + /* +0x06 */ uint8 nfpType; + /* +0x07 */ uint8 unused[0x2F]; + }; + + static_assert(offsetof(RomInfo, amiiboSeries) == 0x3); + static_assert(offsetof(RomInfo, number) == 0x4); + static_assert(offsetof(RomInfo, nfpType) == 0x6); + static_assert(sizeof(RomInfo) == 0x36); + + using ReadOnlyInfo = RomInfo; // same layout + + void GetRomInfo(RomInfo* romInfo) + { + cemu_assert_debug(nfp_data.hasActiveAmiibo); + memset(romInfo, 0x00, sizeof(RomInfo)); + romInfo->characterId[0] = nfp_data.amiiboNFCData.amiiboIdentificationBlock.gameAndCharacterId[0]; + romInfo->characterId[1] = nfp_data.amiiboNFCData.amiiboIdentificationBlock.gameAndCharacterId[1]; + romInfo->characterId[2] = nfp_data.amiiboNFCData.amiiboIdentificationBlock.characterVariation; // guessed + romInfo->amiiboSeries = nfp_data.amiiboNFCData.amiiboIdentificationBlock.amiiboSeries; // guessed + romInfo->number = *(uint16be*)nfp_data.amiiboNFCData.amiiboIdentificationBlock.amiiboModelNumber; // guessed + romInfo->nfpType = nfp_data.amiiboNFCData.amiiboIdentificationBlock.amiiboFigureType; // guessed + memset(romInfo->unused, 0x00, sizeof(romInfo->unused)); } - memset(romInfo, 0x00, sizeof(nfpRomInfo_t)); - romInfo->characterId[0] = nfp_data.amiiboNFCData.amiiboIdentificationBlock.gameAndCharacterId[0]; - romInfo->characterId[1] = nfp_data.amiiboNFCData.amiiboIdentificationBlock.gameAndCharacterId[1]; - romInfo->characterId[2] = nfp_data.amiiboNFCData.amiiboIdentificationBlock.characterVariation; // guessed + nnResult GetNfpRomInfo(RomInfo* romInfo) + { + nnNfpLock(); + if (nfp_data.hasActiveAmiibo == false) + { + nnNfpUnlock(); + return BUILD_NN_RESULT(NN_RESULT_LEVEL_STATUS, NN_RESULT_MODULE_NN_NFP, 0); // todo: Return correct error code + } + GetRomInfo(romInfo); + nnNfpUnlock(); + return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_NFP, 0); + } - romInfo->amiiboSeries = nfp_data.amiiboNFCData.amiiboIdentificationBlock.amiiboSeries; // guessed - romInfo->number = *(uint16be*)nfp_data.amiiboNFCData.amiiboIdentificationBlock.amiiboModelNumber; // guessed - romInfo->nfpType = nfp_data.amiiboNFCData.amiiboIdentificationBlock.amiiboFigureType; // guessed - - nnNfpUnlock(); - osLib_returnFromFunction(hCPU, BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_NFP, 0)); -} + nnResult GetNfpReadOnlyInfo(ReadOnlyInfo* readOnlyInfo) + { + nnNfpLock(); + if (nfp_data.hasActiveAmiibo == false) + { + nnNfpUnlock(); + return BUILD_NN_RESULT(NN_RESULT_LEVEL_STATUS, NN_RESULT_MODULE_NN_NFP, 0); // todo: Return correct error code + } + GetRomInfo(readOnlyInfo); + nnNfpUnlock(); + return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_NFP, 0); + } +}; typedef struct { @@ -880,13 +898,13 @@ void nnNfp_update() if (amiiboElapsedTouchTime >= 1500) { nnNfp_unloadAmiibo(); + if (nfp_data.deactivateEvent) + { + coreinit::OSEvent* osEvent = (coreinit::OSEvent*)memory_getPointerFromVirtualOffset(nfp_data.deactivateEvent); + coreinit::OSSignalEvent(osEvent); + } } nnNfpUnlock(); - if (nfp_data.deactivateEvent) - { - coreinit::OSEvent* osEvent = (coreinit::OSEvent*)memory_getPointerFromVirtualOffset(nfp_data.deactivateEvent); - coreinit::OSSignalEvent(osEvent); - } } void nnNfpExport_GetNfpState(PPCInterpreter_t* hCPU) @@ -1001,8 +1019,6 @@ namespace nn::nfp osLib_addFunction("nn_nfp", "Mount__Q2_2nn3nfpFv", nnNfpExport_Mount); osLib_addFunction("nn_nfp", "MountRom__Q2_2nn3nfpFv", nnNfpExport_MountRom); osLib_addFunction("nn_nfp", "Unmount__Q2_2nn3nfpFv", nnNfpExport_Unmount); - - osLib_addFunction("nn_nfp", "GetNfpRomInfo__Q2_2nn3nfpFPQ3_2nn3nfp7RomInfo", nnNfpExport_GetNfpRomInfo); osLib_addFunction("nn_nfp", "GetNfpCommonInfo__Q2_2nn3nfpFPQ3_2nn3nfp10CommonInfo", nnNfpExport_GetNfpCommonInfo); osLib_addFunction("nn_nfp", "GetNfpRegisterInfo__Q2_2nn3nfpFPQ3_2nn3nfp12RegisterInfo", nnNfpExport_GetNfpRegisterInfo); @@ -1028,7 +1044,9 @@ namespace nn::nfp { nnNfp_load(); // legacy interface, update these to use cafeExportRegister / cafeExportRegisterFunc - cafeExportRegisterFunc(nn::nfp::GetErrorCode, "nn_nfp", "GetErrorCode__Q2_2nn3nfpFRCQ2_2nn6Result", LogType::Placeholder); + cafeExportRegisterFunc(nn::nfp::GetErrorCode, "nn_nfp", "GetErrorCode__Q2_2nn3nfpFRCQ2_2nn6Result", LogType::NN_NFP); + cafeExportRegisterFunc(nn::nfp::GetNfpRomInfo, "nn_nfp", "GetNfpRomInfo__Q2_2nn3nfpFPQ3_2nn3nfp7RomInfo", LogType::NN_NFP); + cafeExportRegisterFunc(nn::nfp::GetNfpReadOnlyInfo, "nn_nfp", "GetNfpReadOnlyInfo__Q2_2nn3nfpFPQ3_2nn3nfp12ReadOnlyInfo", LogType::NN_NFP); } } From 9941e00b545a9c99a8e62c8c33ebe790d38de26e Mon Sep 17 00:00:00 2001 From: SamoZ256 <96914946+SamoZ256@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:22:00 +0100 Subject: [PATCH 032/137] macOS: Fix libusb path for bundle (#1403) --- src/CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7d64d91b..84d4fcad 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -101,12 +101,18 @@ if (MACOS_BUNDLE) COMMAND ${CMAKE_COMMAND} ARGS -E copy_directory "${CMAKE_SOURCE_DIR}/bin/${folder}" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/SharedSupport/${folder}") endforeach(folder) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(LIBUSB_PATH "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-osx/debug/lib/libusb-1.0.0.dylib") + else() + set(LIBUSB_PATH "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-osx/lib/libusb-1.0.0.dylib") + endif() + add_custom_command (TARGET CemuBin POST_BUILD COMMAND ${CMAKE_COMMAND} ARGS -E copy "/usr/local/lib/libMoltenVK.dylib" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libMoltenVK.dylib" - COMMAND ${CMAKE_COMMAND} ARGS -E copy "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-osx/lib/libusb-1.0.0.dylib" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib" + COMMAND ${CMAKE_COMMAND} ARGS -E copy "${LIBUSB_PATH}" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib" COMMAND ${CMAKE_COMMAND} ARGS -E copy "${CMAKE_SOURCE_DIR}/src/resource/update.sh" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/update.sh" COMMAND bash -c "install_name_tool -add_rpath @executable_path/../Frameworks ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}" - COMMAND bash -c "install_name_tool -change /Users/runner/work/Cemu/Cemu/build/vcpkg_installed/x64-osx/lib/libusb-1.0.0.dylib @executable_path/../Frameworks/libusb-1.0.0.dylib ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}") + COMMAND bash -c "install_name_tool -change ${LIBUSB_PATH} ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}") endif() set_target_properties(CemuBin PROPERTIES From 813f9148b1afe75053102877e3db5733f701660e Mon Sep 17 00:00:00 2001 From: SamoZ256 <96914946+SamoZ256@users.noreply.github.com> Date: Thu, 7 Nov 2024 07:09:35 +0100 Subject: [PATCH 033/137] macOS: Fix absolute path to libusb dylib (#1405) --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 84d4fcad..3792ab85 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -112,7 +112,7 @@ if (MACOS_BUNDLE) COMMAND ${CMAKE_COMMAND} ARGS -E copy "${LIBUSB_PATH}" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib" COMMAND ${CMAKE_COMMAND} ARGS -E copy "${CMAKE_SOURCE_DIR}/src/resource/update.sh" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/update.sh" COMMAND bash -c "install_name_tool -add_rpath @executable_path/../Frameworks ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}" - COMMAND bash -c "install_name_tool -change ${LIBUSB_PATH} ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}") + COMMAND bash -c "install_name_tool -change ${LIBUSB_PATH} @executable_path/../Frameworks/libusb-1.0.0.dylib ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}") endif() set_target_properties(CemuBin PROPERTIES From 4ac1ab162a2a2734a4b1839e57ff367233e16853 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sat, 9 Nov 2024 05:21:06 +0000 Subject: [PATCH 034/137] procui: swap `tickDelay` and `priority` args in callbacks (#1408) --- src/Cafe/OS/libs/proc_ui/proc_ui.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cafe/OS/libs/proc_ui/proc_ui.cpp b/src/Cafe/OS/libs/proc_ui/proc_ui.cpp index dd9a460f..ff38abbb 100644 --- a/src/Cafe/OS/libs/proc_ui/proc_ui.cpp +++ b/src/Cafe/OS/libs/proc_ui/proc_ui.cpp @@ -427,7 +427,7 @@ namespace proc_ui } if(callbackType != ProcUICallbackId::AcquireForeground) priority = -priority; - AddCallbackInternal(funcPtr, userParam, priority, 0, s_CallbackTables[stdx::to_underlying(callbackType)][coreIndex]); + AddCallbackInternal(funcPtr, userParam, 0, priority, s_CallbackTables[stdx::to_underlying(callbackType)][coreIndex]); } void ProcUIRegisterCallback(ProcUICallbackId callbackType, void* funcPtr, void* userParam, sint32 priority) @@ -437,7 +437,7 @@ namespace proc_ui void ProcUIRegisterBackgroundCallback(void* funcPtr, void* userParam, uint64 tickDelay) { - AddCallbackInternal(funcPtr, userParam, 0, tickDelay, s_backgroundCallbackList); + AddCallbackInternal(funcPtr, userParam, tickDelay, 0, s_backgroundCallbackList); } void FreeCallbackChain(ProcUICallbackList& callbackList) From 2e829479d9f63dcfbd8ef67d456793a70f684b18 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sat, 9 Nov 2024 05:22:13 +0000 Subject: [PATCH 035/137] nsyshid/libusb: correct error message formatting and print error string on open fail (#1407) --- src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp index 7548c998..ab355136 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp @@ -15,7 +15,7 @@ namespace nsyshid::backend::libusb if (m_initReturnCode < 0) { m_ctx = nullptr; - cemuLog_logDebug(LogType::Force, "nsyshid::BackendLibusb: failed to initialize libusb with return code %i", + cemuLog_logDebug(LogType::Force, "nsyshid::BackendLibusb: failed to initialize libusb, return code: {}", m_initReturnCode); return; } @@ -35,7 +35,7 @@ namespace nsyshid::backend::libusb if (ret != LIBUSB_SUCCESS) { cemuLog_logDebug(LogType::Force, - "nsyshid::BackendLibusb: failed to register hotplug callback with return code %i", + "nsyshid::BackendLibusb: failed to register hotplug callback with return code {}", ret); } else @@ -415,7 +415,7 @@ namespace nsyshid::backend::libusb if (ret < 0) { cemuLog_log(LogType::Force, - "nsyshid::DeviceLibusb::open(): failed to get device descriptor; return code: %i", + "nsyshid::DeviceLibusb::open(): failed to get device descriptor, return code: {}", ret); libusb_free_device_list(devices, 1); return false; @@ -439,8 +439,8 @@ namespace nsyshid::backend::libusb { this->m_libusbHandle = nullptr; cemuLog_log(LogType::Force, - "nsyshid::DeviceLibusb::open(): failed to open device; return code: %i", - ret); + "nsyshid::DeviceLibusb::open(): failed to open device: {}", + libusb_strerror(ret)); libusb_free_device_list(devices, 1); return false; } From ca2e0a7c31dcaeac110dc4aa703a992b55c8155f Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Mon, 11 Nov 2024 07:58:01 +0000 Subject: [PATCH 036/137] nsyshid: Add support for emulated Dimensions Toypad (#1371) --- src/Cafe/CMakeLists.txt | 2 + src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp | 9 + src/Cafe/OS/libs/nsyshid/Dimensions.cpp | 1162 +++++++++++++++++ src/Cafe/OS/libs/nsyshid/Dimensions.h | 108 ++ src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 1 + .../EmulatedUSBDeviceFrame.cpp | 410 +++++- .../EmulatedUSBDeviceFrame.h | 48 +- 8 files changed, 1690 insertions(+), 52 deletions(-) create mode 100644 src/Cafe/OS/libs/nsyshid/Dimensions.cpp create mode 100644 src/Cafe/OS/libs/nsyshid/Dimensions.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 91d257b2..0901fece 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -465,6 +465,8 @@ add_library(CemuCafe OS/libs/nsyshid/BackendLibusb.h OS/libs/nsyshid/BackendWindowsHID.cpp OS/libs/nsyshid/BackendWindowsHID.h + OS/libs/nsyshid/Dimensions.cpp + OS/libs/nsyshid/Dimensions.h OS/libs/nsyshid/Infinity.cpp OS/libs/nsyshid/Infinity.h OS/libs/nsyshid/Skylander.cpp diff --git a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp index 95eaf06a..533d349e 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp @@ -1,4 +1,6 @@ #include "BackendEmulated.h" + +#include "Dimensions.h" #include "Infinity.h" #include "Skylander.h" #include "config/CemuConfig.h" @@ -33,5 +35,12 @@ namespace nsyshid::backend::emulated auto device = std::make_shared(); AttachDevice(device); } + if (GetConfig().emulated_usb_devices.emulate_dimensions_toypad && !FindDeviceById(0x0E6F, 0x0241)) + { + cemuLog_logDebug(LogType::Force, "Attaching Emulated Toypad"); + // Add Dimensions Toypad + auto device = std::make_shared(); + AttachDevice(device); + } } } // namespace nsyshid::backend::emulated \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Dimensions.cpp b/src/Cafe/OS/libs/nsyshid/Dimensions.cpp new file mode 100644 index 00000000..f328dde7 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Dimensions.cpp @@ -0,0 +1,1162 @@ +#include "Dimensions.h" + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +#include +#include + +namespace nsyshid +{ + static constexpr std::array COMMAND_KEY = {0x55, 0xFE, 0xF6, 0xB0, 0x62, 0xBF, 0x0B, 0x41, + 0xC9, 0xB3, 0x7C, 0xB4, 0x97, 0x3E, 0x29, 0x7B}; + + static constexpr std::array CHAR_CONSTANT = {0xB7, 0xD5, 0xD7, 0xE6, 0xE7, 0xBA, 0x3C, 0xA8, + 0xD8, 0x75, 0x47, 0x68, 0xCF, 0x23, 0xE9, 0xFE, 0xAA}; + + static constexpr std::array PWD_CONSTANT = {0x28, 0x63, 0x29, 0x20, 0x43, 0x6F, 0x70, 0x79, + 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4C, 0x45, + 0x47, 0x4F, 0x20, 0x32, 0x30, 0x31, 0x34, 0xAA, 0xAA}; + + DimensionsUSB g_dimensionstoypad; + + const std::map s_listMinis = { + {0, "Blank Tag"}, + {1, "Batman"}, + {2, "Gandalf"}, + {3, "Wyldstyle"}, + {4, "Aquaman"}, + {5, "Bad Cop"}, + {6, "Bane"}, + {7, "Bart Simpson"}, + {8, "Benny"}, + {9, "Chell"}, + {10, "Cole"}, + {11, "Cragger"}, + {12, "Cyborg"}, + {13, "Cyberman"}, + {14, "Doc Brown"}, + {15, "The Doctor"}, + {16, "Emmet"}, + {17, "Eris"}, + {18, "Gimli"}, + {19, "Gollum"}, + {20, "Harley Quinn"}, + {21, "Homer Simpson"}, + {22, "Jay"}, + {23, "Joker"}, + {24, "Kai"}, + {25, "ACU Trooper"}, + {26, "Gamer Kid"}, + {27, "Krusty the Clown"}, + {28, "Laval"}, + {29, "Legolas"}, + {30, "Lloyd"}, + {31, "Marty McFly"}, + {32, "Nya"}, + {33, "Owen Grady"}, + {34, "Peter Venkman"}, + {35, "Slimer"}, + {36, "Scooby-Doo"}, + {37, "Sensei Wu"}, + {38, "Shaggy"}, + {39, "Stay Puft"}, + {40, "Superman"}, + {41, "Unikitty"}, + {42, "Wicked Witch of the West"}, + {43, "Wonder Woman"}, + {44, "Zane"}, + {45, "Green Arrow"}, + {46, "Supergirl"}, + {47, "Abby Yates"}, + {48, "Finn the Human"}, + {49, "Ethan Hunt"}, + {50, "Lumpy Space Princess"}, + {51, "Jake the Dog"}, + {52, "Harry Potter"}, + {53, "Lord Voldemort"}, + {54, "Michael Knight"}, + {55, "B.A. Baracus"}, + {56, "Newt Scamander"}, + {57, "Sonic the Hedgehog"}, + {58, "Future Update (unreleased)"}, + {59, "Gizmo"}, + {60, "Stripe"}, + {61, "E.T."}, + {62, "Tina Goldstein"}, + {63, "Marceline the Vampire Queen"}, + {64, "Batgirl"}, + {65, "Robin"}, + {66, "Sloth"}, + {67, "Hermione Granger"}, + {68, "Chase McCain"}, + {69, "Excalibur Batman"}, + {70, "Raven"}, + {71, "Beast Boy"}, + {72, "Betelgeuse"}, + {73, "Lord Vortech (unreleased)"}, + {74, "Blossom"}, + {75, "Bubbles"}, + {76, "Buttercup"}, + {77, "Starfire"}, + {78, "World 15 (unreleased)"}, + {79, "World 16 (unreleased)"}, + {80, "World 17 (unreleased)"}, + {81, "World 18 (unreleased)"}, + {82, "World 19 (unreleased)"}, + {83, "World 20 (unreleased)"}, + {768, "Unknown 768"}, + {769, "Supergirl Red Lantern"}, + {770, "Unknown 770"}}; + + const std::map s_listTokens = { + {1000, "Police Car"}, + {1001, "Aerial Squad Car"}, + {1002, "Missile Striker"}, + {1003, "Gravity Sprinter"}, + {1004, "Street Shredder"}, + {1005, "Sky Clobberer"}, + {1006, "Batmobile"}, + {1007, "Batblaster"}, + {1008, "Sonic Batray"}, + {1009, "Benny's Spaceship"}, + {1010, "Lasercraft"}, + {1011, "The Annihilator"}, + {1012, "DeLorean Time Machine"}, + {1013, "Electric Time Machine"}, + {1014, "Ultra Time Machine"}, + {1015, "Hoverboard"}, + {1016, "Cyclone Board"}, + {1017, "Ultimate Hoverjet"}, + {1018, "Eagle Interceptor"}, + {1019, "Eagle Sky Blazer"}, + {1020, "Eagle Swoop Diver"}, + {1021, "Swamp Skimmer"}, + {1022, "Cragger's Fireship"}, + {1023, "Croc Command Sub"}, + {1024, "Cyber-Guard"}, + {1025, "Cyber-Wrecker"}, + {1026, "Laser Robot Walker"}, + {1027, "K-9"}, + {1028, "K-9 Ruff Rover"}, + {1029, "K-9 Laser Cutter"}, + {1030, "TARDIS"}, + {1031, "Laser-Pulse TARDIS"}, + {1032, "Energy-Burst TARDIS"}, + {1033, "Emmet's Excavator"}, + {1034, "Destroy Dozer"}, + {1035, "Destruct-o-Mech"}, + {1036, "Winged Monkey"}, + {1037, "Battle Monkey"}, + {1038, "Commander Monkey"}, + {1039, "Axe Chariot"}, + {1040, "Axe Hurler"}, + {1041, "Soaring Chariot"}, + {1042, "Shelob the Great"}, + {1043, "8-Legged Stalker"}, + {1044, "Poison Slinger"}, + {1045, "Homer's Car"}, + {1047, "The SubmaHomer"}, + {1046, "The Homercraft"}, + {1048, "Taunt-o-Vision"}, + {1050, "The MechaHomer"}, + {1049, "Blast Cam"}, + {1051, "Velociraptor"}, + {1053, "Venom Raptor"}, + {1052, "Spike Attack Raptor"}, + {1054, "Gyrosphere"}, + {1055, "Sonic Beam Gyrosphere"}, + {1056, " Gyrosphere"}, + {1057, "Clown Bike"}, + {1058, "Cannon Bike"}, + {1059, "Anti-Gravity Rocket Bike"}, + {1060, "Mighty Lion Rider"}, + {1061, "Lion Blazer"}, + {1062, "Fire Lion"}, + {1063, "Arrow Launcher"}, + {1064, "Seeking Shooter"}, + {1065, "Triple Ballista"}, + {1066, "Mystery Machine"}, + {1067, "Mystery Tow & Go"}, + {1068, "Mystery Monster"}, + {1069, "Boulder Bomber"}, + {1070, "Boulder Blaster"}, + {1071, "Cyclone Jet"}, + {1072, "Storm Fighter"}, + {1073, "Lightning Jet"}, + {1074, "Electro-Shooter"}, + {1075, "Blade Bike"}, + {1076, "Flight Fire Bike"}, + {1077, "Blades of Fire"}, + {1078, "Samurai Mech"}, + {1079, "Samurai Shooter"}, + {1080, "Soaring Samurai Mech"}, + {1081, "Companion Cube"}, + {1082, "Laser Deflector"}, + {1083, "Gold Heart Emitter"}, + {1084, "Sentry Turret"}, + {1085, "Turret Striker"}, + {1086, "Flight Turret Carrier"}, + {1087, "Scooby Snack"}, + {1088, "Scooby Fire Snack"}, + {1089, "Scooby Ghost Snack"}, + {1090, "Cloud Cuckoo Car"}, + {1091, "X-Stream Soaker"}, + {1092, "Rainbow Cannon"}, + {1093, "Invisible Jet"}, + {1094, "Laser Shooter"}, + {1095, "Torpedo Bomber"}, + {1096, "NinjaCopter"}, + {1097, "Glaciator"}, + {1098, "Freeze Fighter"}, + {1099, "Travelling Time Train"}, + {1100, "Flight Time Train"}, + {1101, "Missile Blast Time Train"}, + {1102, "Aqua Watercraft"}, + {1103, "Seven Seas Speeder"}, + {1104, "Trident of Fire"}, + {1105, "Drill Driver"}, + {1106, "Bane Dig 'n' Drill"}, + {1107, "Bane Drill 'n' Blast"}, + {1108, "Quinn Mobile"}, + {1109, "Quinn Ultra Racer"}, + {1110, "Missile Launcher"}, + {1111, "The Joker's Chopper"}, + {1112, "Mischievous Missile Blaster"}, + {1113, "Lock 'n' Laser Jet"}, + {1114, "Hover Pod"}, + {1115, "Krypton Striker"}, + {1116, "Super Stealth Pod"}, + {1117, "Dalek"}, + {1118, "Fire 'n' Ride Dalek"}, + {1119, "Silver Shooter Dalek"}, + {1120, "Ecto-1"}, + {1121, "Ecto-1 Blaster"}, + {1122, "Ecto-1 Water Diver"}, + {1123, "Ghost Trap"}, + {1124, "Ghost Stun 'n' Trap"}, + {1125, "Proton Zapper"}, + {1126, "Unknown"}, + {1127, "Unknown"}, + {1128, "Unknown"}, + {1129, "Unknown"}, + {1130, "Unknown"}, + {1131, "Unknown"}, + {1132, "Lloyd's Golden Dragon"}, + {1133, "Sword Projector Dragon"}, + {1134, "Unknown"}, + {1135, "Unknown"}, + {1136, "Unknown"}, + {1137, "Unknown"}, + {1138, "Unknown"}, + {1139, "Unknown"}, + {1140, "Unknown"}, + {1141, "Unknown"}, + {1142, "Unknown"}, + {1143, "Unknown"}, + {1144, "Mega Flight Dragon"}, + {1145, "Unknown"}, + {1146, "Unknown"}, + {1147, "Unknown"}, + {1148, "Unknown"}, + {1149, "Unknown"}, + {1150, "Unknown"}, + {1151, "Unknown"}, + {1152, "Unknown"}, + {1153, "Unknown"}, + {1154, "Unknown"}, + {1155, "Flying White Dragon"}, + {1156, "Golden Fire Dragon"}, + {1157, "Ultra Destruction Dragon"}, + {1158, "Arcade Machine"}, + {1159, "8-Bit Shooter"}, + {1160, "The Pixelator"}, + {1161, "G-6155 Spy Hunter"}, + {1162, "Interdiver"}, + {1163, "Aerial Spyhunter"}, + {1164, "Slime Shooter"}, + {1165, "Slime Exploder"}, + {1166, "Slime Streamer"}, + {1167, "Terror Dog"}, + {1168, "Terror Dog Destroyer"}, + {1169, "Soaring Terror Dog"}, + {1170, "Ancient Psychic Tandem War Elephant"}, + {1171, "Cosmic Squid"}, + {1172, "Psychic Submarine"}, + {1173, "BMO"}, + {1174, "DOGMO"}, + {1175, "SNAKEMO"}, + {1176, "Jakemobile"}, + {1177, "Snail Dude Jake"}, + {1178, "Hover Jake"}, + {1179, "Lumpy Car"}, + {1181, "Lumpy Land Whale"}, + {1180, "Lumpy Truck"}, + {1182, "Lunatic Amp"}, + {1183, "Shadow Scorpion"}, + {1184, "Heavy Metal Monster"}, + {1185, "B.A.'s Van"}, + {1186, "Fool Smasher"}, + {1187, "Pain Plane"}, + {1188, "Phone Home"}, + {1189, "Mobile Uplink"}, + {1190, "Super-Charged Satellite"}, + {1191, "Niffler"}, + {1192, "Sinister Scorpion"}, + {1193, "Vicious Vulture"}, + {1194, "Swooping Evil"}, + {1195, "Brutal Bloom"}, + {1196, "Crawling Creeper"}, + {1197, "Ecto-1 (2016)"}, + {1198, "Ectozer"}, + {1199, "PerfEcto"}, + {1200, "Flash 'n' Finish"}, + {1201, "Rampage Record Player"}, + {1202, "Stripe's Throne"}, + {1203, "R.C. Racer"}, + {1204, "Gadget-O-Matic"}, + {1205, "Scarlet Scorpion"}, + {1206, "Hogwarts Express"}, + {1208, "Steam Warrior"}, + {1207, "Soaring Steam Plane"}, + {1209, "Enchanted Car"}, + {1210, "Shark Sub"}, + {1211, "Monstrous Mouth"}, + {1212, "IMF Scrambler"}, + {1213, "Shock Cycle"}, + {1214, "IMF Covert Jet"}, + {1215, "IMF Sports Car"}, + {1216, "IMF Tank"}, + {1217, "IMF Splorer"}, + {1218, "Sonic Speedster"}, + {1219, "Blue Typhoon"}, + {1220, "Moto Bug"}, + {1221, "The Tornado"}, + {1222, "Crabmeat"}, + {1223, "Eggcatcher"}, + {1224, "K.I.T.T."}, + {1225, "Goliath Armored Semi"}, + {1226, "K.I.T.T. Jet"}, + {1227, "Police Helicopter"}, + {1228, "Police Hovercraft"}, + {1229, "Police Plane"}, + {1230, "Bionic Steed"}, + {1231, "Bat-Raptor"}, + {1232, "Ultrabat"}, + {1233, "Batwing"}, + {1234, "The Black Thunder"}, + {1235, "Bat-Tank"}, + {1236, "Skeleton Organ"}, + {1237, "Skeleton Jukebox"}, + {1238, "Skele-Turkey"}, + {1239, "One-Eyed Willy's Pirate Ship"}, + {1240, "Fanged Fortune"}, + {1241, "Inferno Cannon"}, + {1242, "Buckbeak"}, + {1243, "Giant Owl"}, + {1244, "Fierce Falcon"}, + {1245, "Saturn's Sandworm"}, + {1247, "Haunted Vacuum"}, + {1246, "Spooky Spider"}, + {1248, "PPG Smartphone"}, + {1249, "PPG Hotline"}, + {1250, "Powerpuff Mag-Net"}, + {1253, "Mega Blast Bot"}, + {1251, "Ka-Pow Cannon"}, + {1252, "Slammin' Guitar"}, + {1254, "Octi"}, + {1255, "Super Skunk"}, + {1256, "Sonic Squid"}, + {1257, "T-Car"}, + {1258, "T-Forklift"}, + {1259, "T-Plane"}, + {1260, "Spellbook of Azarath"}, + {1261, "Raven Wings"}, + {1262, "Giant Hand"}, + {1263, "Titan Robot"}, + {1264, "T-Rocket"}, + {1265, "Robot Retriever"}}; + + DimensionsToypadDevice::DimensionsToypadDevice() + : Device(0x0E6F, 0x0241, 1, 2, 0) + { + m_IsOpened = false; + } + + bool DimensionsToypadDevice::Open() + { + if (!IsOpened()) + { + m_IsOpened = true; + } + return true; + } + + void DimensionsToypadDevice::Close() + { + if (IsOpened()) + { + m_IsOpened = false; + } + } + + bool DimensionsToypadDevice::IsOpened() + { + return m_IsOpened; + } + + Device::ReadResult DimensionsToypadDevice::Read(ReadMessage* message) + { + memcpy(message->data, g_dimensionstoypad.GetStatus().data(), message->length); + message->bytesRead = message->length; + return Device::ReadResult::Success; + } + + Device::WriteResult DimensionsToypadDevice::Write(WriteMessage* message) + { + if (message->length != 32) + return Device::WriteResult::Error; + + g_dimensionstoypad.SendCommand(std::span{message->data, 32}); + message->bytesWritten = message->length; + return Device::WriteResult::Success; + } + + bool DimensionsToypadDevice::GetDescriptor(uint8 descType, + uint8 descIndex, + uint8 lang, + uint8* output, + uint32 outputMaxLength) + { + uint8 configurationDescriptor[0x29]; + + uint8* currentWritePtr; + + // configuration descriptor + currentWritePtr = configurationDescriptor + 0; + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 2; // bDescriptorType + *(uint16be*)(currentWritePtr + 2) = 0x0029; // wTotalLength + *(uint8*)(currentWritePtr + 4) = 1; // bNumInterfaces + *(uint8*)(currentWritePtr + 5) = 1; // bConfigurationValue + *(uint8*)(currentWritePtr + 6) = 0; // iConfiguration + *(uint8*)(currentWritePtr + 7) = 0x80; // bmAttributes + *(uint8*)(currentWritePtr + 8) = 0xFA; // MaxPower + currentWritePtr = currentWritePtr + 9; + // configuration descriptor + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 0x04; // bDescriptorType + *(uint8*)(currentWritePtr + 2) = 0; // bInterfaceNumber + *(uint8*)(currentWritePtr + 3) = 0; // bAlternateSetting + *(uint8*)(currentWritePtr + 4) = 2; // bNumEndpoints + *(uint8*)(currentWritePtr + 5) = 3; // bInterfaceClass + *(uint8*)(currentWritePtr + 6) = 0; // bInterfaceSubClass + *(uint8*)(currentWritePtr + 7) = 0; // bInterfaceProtocol + *(uint8*)(currentWritePtr + 8) = 0; // iInterface + currentWritePtr = currentWritePtr + 9; + // configuration descriptor + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 0x21; // bDescriptorType + *(uint16be*)(currentWritePtr + 2) = 0x0111; // bcdHID + *(uint8*)(currentWritePtr + 4) = 0x00; // bCountryCode + *(uint8*)(currentWritePtr + 5) = 0x01; // bNumDescriptors + *(uint8*)(currentWritePtr + 6) = 0x22; // bDescriptorType + *(uint16be*)(currentWritePtr + 7) = 0x001D; // wDescriptorLength + currentWritePtr = currentWritePtr + 9; + // endpoint descriptor 1 + *(uint8*)(currentWritePtr + 0) = 7; // bLength + *(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType + *(uint8*)(currentWritePtr + 2) = 0x81; // bEndpointAddress + *(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 4) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 6) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + // endpoint descriptor 2 + *(uint8*)(currentWritePtr + 0) = 7; // bLength + *(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType + *(uint8*)(currentWritePtr + 1) = 0x02; // bEndpointAddress + *(uint8*)(currentWritePtr + 2) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 3) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 5) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + + cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29); + + memcpy(output, configurationDescriptor, + std::min(outputMaxLength, sizeof(configurationDescriptor))); + return true; + } + + bool DimensionsToypadDevice::SetProtocol(uint8 ifIndex, uint8 protocol) + { + cemuLog_log(LogType::Force, "Toypad Protocol"); + return true; + } + + bool DimensionsToypadDevice::SetReport(ReportMessage* message) + { + cemuLog_log(LogType::Force, "Toypad Report"); + return true; + } + + std::array DimensionsUSB::GetStatus() + { + std::array response = {}; + + bool responded = false; + do + { + if (!m_queries.empty()) + { + response = m_queries.front(); + m_queries.pop(); + responded = true; + } + else if (!m_figureAddedRemovedResponses.empty() && m_isAwake) + { + std::lock_guard lock(m_dimensionsMutex); + response = m_figureAddedRemovedResponses.front(); + m_figureAddedRemovedResponses.pop(); + responded = true; + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + while (responded == false); + return response; + } + + void DimensionsUSB::SendCommand(std::span buf) + { + const uint8 command = buf[2]; + const uint8 sequence = buf[3]; + + std::array q_result{}; + + switch (command) + { + case 0xB0: // Wake + { + // Consistent device response to the wake command + q_result = {0x55, 0x0e, 0x01, 0x28, 0x63, 0x29, + 0x20, 0x4c, 0x45, 0x47, 0x4f, 0x20, + 0x32, 0x30, 0x31, 0x34, 0x46}; + break; + } + case 0xB1: // Seed + { + // Initialise a random number generator using the seed provided + g_dimensionstoypad.GenerateRandomNumber(std::span{buf.begin() + 4, 8}, sequence, q_result); + break; + } + case 0xB3: // Challenge + { + // Get the next number in the sequence based on the RNG from 0xB1 command + g_dimensionstoypad.GetChallengeResponse(std::span{buf.begin() + 4, 8}, sequence, q_result); + break; + } + case 0xC0: // Color + case 0xC1: // Get Pad Color + case 0xC2: // Fade + case 0xC3: // Flash + case 0xC4: // Fade Random + case 0xC6: // Fade All + case 0xC7: // Flash All + case 0xC8: // Color All + { + // Send a blank response to acknowledge color has been sent to toypad + q_result = {0x55, 0x01, sequence}; + q_result[3] = GenerateChecksum(q_result, 3); + break; + } + case 0xD2: // Read + { + // Read 4 pages from the figure at index (buf[4]), starting with page buf[5] + g_dimensionstoypad.QueryBlock(buf[4], buf[5], q_result, sequence); + break; + } + case 0xD3: // Write + { + // Write 4 bytes to page buf[5] to the figure at index buf[4] + g_dimensionstoypad.WriteBlock(buf[4], buf[5], std::span{buf.begin() + 6, 4}, q_result, sequence); + break; + } + case 0xD4: // Model + { + // Get the model id of the figure at index buf[4] + g_dimensionstoypad.GetModel(std::span{buf.begin() + 4, 8}, sequence, q_result); + break; + } + case 0xD0: // Tag List + case 0xE1: // PWD + case 0xE5: // Active + case 0xFF: // LEDS Query + { + // Further investigation required + cemuLog_log(LogType::Force, "Unimplemented LD Function: {:x}", command); + break; + } + default: + { + cemuLog_log(LogType::Force, "Unknown LD Function: {:x}", command); + break; + } + } + + m_queries.push(q_result); + } + + uint32 DimensionsUSB::LoadFigure(const std::array& buf, std::unique_ptr file, uint8 pad, uint8 index) + { + std::lock_guard lock(m_dimensionsMutex); + + const uint32 id = GetFigureId(buf); + + DimensionsMini& figure = GetFigureByIndex(index); + figure.dimFile = std::move(file); + figure.id = id; + figure.pad = pad; + figure.index = index + 1; + figure.data = buf; + // When a figure is added to the toypad, respond to the game with the pad they were added to, their index, + // the direction (0x00 in byte 6 for added) and their UID + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return id; + } + + bool DimensionsUSB::RemoveFigure(uint8 pad, uint8 index, bool fullRemove) + { + std::lock_guard lock(m_dimensionsMutex); + + DimensionsMini& figure = GetFigureByIndex(index); + if (figure.index == 255) + return false; + + // When a figure is removed from the toypad, respond to the game with the pad they were removed from, their index, + // the direction (0x01 in byte 6 for removed) and their UID + if (fullRemove) + { + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, + figure.data[0], figure.data[1], figure.data[2], + figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + figure.Save(); + figure.dimFile.reset(); + } + + figure.index = 255; + figure.pad = 255; + figure.id = 0; + + return true; + } + + bool DimensionsUSB::TempRemove(uint8 index) + { + std::lock_guard lock(m_dimensionsMutex); + + DimensionsMini& figure = GetFigureByIndex(index); + if (figure.index == 255) + return false; + + // Send a response to the game that the figure has been "Picked up" from existing slot, + // until either the movement is cancelled, or user chooses a space to move to + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, + figure.data[0], figure.data[1], figure.data[2], + figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; + + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + } + + bool DimensionsUSB::CancelRemove(uint8 index) + { + std::lock_guard lock(m_dimensionsMutex); + + DimensionsMini& figure = GetFigureByIndex(index); + if (figure.index == 255) + return false; + + // Cancel the previous movement of the figure + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, + figure.data[0], figure.data[1], figure.data[2], + figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; + + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return true; + } + + bool DimensionsUSB::CreateFigure(fs::path pathName, uint32 id) + { + FileStream* dimFile(FileStream::createFile2(pathName)); + if (!dimFile) + return false; + + std::array fileData{}; + RandomUID(fileData); + fileData[3] = id & 0xFF; + + std::array uid = {fileData[0], fileData[1], fileData[2], fileData[4], fileData[5], fileData[6], fileData[7]}; + + // Only characters are created with their ID encrypted and stored in pages 36 and 37, + // as well as a password stored in page 43. Blank tags have their information populated + // by the game when it calls the write_block command. + if (id != 0) + { + const std::array figureKey = GenerateFigureKey(fileData); + + std::array valueToEncrypt = {uint8(id & 0xFF), uint8((id >> 8) & 0xFF), uint8((id >> 16) & 0xFF), uint8((id >> 24) & 0xFF), + uint8(id & 0xFF), uint8((id >> 8) & 0xFF), uint8((id >> 16) & 0xFF), uint8((id >> 24) & 0xFF)}; + + std::array encrypted = Encrypt(valueToEncrypt, figureKey); + + std::memcpy(&fileData[36 * 4], &encrypted[0], 4); + std::memcpy(&fileData[37 * 4], &encrypted[4], 4); + + std::memcpy(&fileData[43 * 4], PWDGenerate(fileData).data(), 4); + } + else + { + // Page 38 is used as verification for blank tags + fileData[(38 * 4) + 1] = 1; + } + + if (fileData.size() != dimFile->writeData(fileData.data(), fileData.size())) + { + delete dimFile; + return false; + } + delete dimFile; + return true; + } + + bool DimensionsUSB::MoveFigure(uint8 pad, uint8 index, uint8 oldPad, uint8 oldIndex) + { + if (oldIndex == index) + { + // Don't bother removing and loading again, just send response to the game + CancelRemove(index); + return true; + } + + // When moving figures between spaces on the toypad, remove any figure from the space they are moving to, + // then remove them from their current space, then load them to the space they are moving to + RemoveFigure(pad, index, true); + + DimensionsMini& figure = GetFigureByIndex(oldIndex); + const std::array data = figure.data; + std::unique_ptr inFile = std::move(figure.dimFile); + + RemoveFigure(oldPad, oldIndex, false); + + LoadFigure(data, std::move(inFile), pad, index); + + return true; + } + + void DimensionsUSB::GenerateRandomNumber(std::span buf, uint8 sequence, + std::array& replyBuf) + { + // Decrypt payload into an 8 byte array + std::array value = Decrypt(buf, std::nullopt); + // Seed is the first 4 bytes (little endian) of the decrypted payload + uint32 seed = (uint32&)value[0]; + // Confirmation is the second 4 bytes (big endian) of the decrypted payload + uint32 conf = (uint32be&)value[4]; + // Initialize rng using the seed from decrypted payload + InitializeRNG(seed); + // Encrypt 8 bytes, first 4 bytes is the decrypted confirmation from payload, 2nd 4 bytes are blank + std::array valueToEncrypt = {value[4], value[5], value[6], value[7], 0, 0, 0, 0}; + std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); + replyBuf[0] = 0x55; + replyBuf[1] = 0x09; + replyBuf[2] = sequence; + // Copy encrypted value to response data + memcpy(&replyBuf[3], encrypted.data(), encrypted.size()); + replyBuf[11] = GenerateChecksum(replyBuf, 11); + } + + void DimensionsUSB::GetChallengeResponse(std::span buf, uint8 sequence, + std::array& replyBuf) + { + // Decrypt payload into an 8 byte array + std::array value = Decrypt(buf, std::nullopt); + // Confirmation is the first 4 bytes of the decrypted payload + uint32 conf = (uint32be&)value[0]; + // Generate next random number based on RNG + uint32 nextRandom = GetNext(); + // Encrypt an 8 byte array, first 4 bytes are the next random number (little endian) + // followed by the confirmation from the decrypted payload + std::array valueToEncrypt = {uint8(nextRandom & 0xFF), uint8((nextRandom >> 8) & 0xFF), + uint8((nextRandom >> 16) & 0xFF), uint8((nextRandom >> 24) & 0xFF), + value[0], value[1], value[2], value[3]}; + std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); + replyBuf[0] = 0x55; + replyBuf[1] = 0x09; + replyBuf[2] = sequence; + // Copy encrypted value to response data + memcpy(&replyBuf[3], encrypted.data(), encrypted.size()); + replyBuf[11] = GenerateChecksum(replyBuf, 11); + + if (!m_isAwake) + m_isAwake = true; + } + + void DimensionsUSB::InitializeRNG(uint32 seed) + { + m_randomA = 0xF1EA5EED; + m_randomB = seed; + m_randomC = seed; + m_randomD = seed; + + for (int i = 0; i < 42; i++) + { + GetNext(); + } + } + + uint32 DimensionsUSB::GetNext() + { + uint32 e = m_randomA - std::rotl(m_randomB, 21); + m_randomA = m_randomB ^ std::rotl(m_randomC, 19); + m_randomB = m_randomC + std::rotl(m_randomD, 6); + m_randomC = m_randomD + e; + m_randomD = e + m_randomA; + return m_randomD; + } + + std::array DimensionsUSB::Decrypt(std::span buf, std::optional> key) + { + // Value to decrypt is separated in to two little endian 32 bit unsigned integers + uint32 dataOne = (uint32&)buf[0]; + uint32 dataTwo = (uint32&)buf[4]; + + // Use the key as 4 32 bit little endian unsigned integers + uint32 keyOne; + uint32 keyTwo; + uint32 keyThree; + uint32 keyFour; + + if (key) + { + keyOne = (uint32&)key.value()[0]; + keyTwo = (uint32&)key.value()[4]; + keyThree = (uint32&)key.value()[8]; + keyFour = (uint32&)key.value()[12]; + } + else + { + keyOne = (uint32&)COMMAND_KEY[0]; + keyTwo = (uint32&)COMMAND_KEY[4]; + keyThree = (uint32&)COMMAND_KEY[8]; + keyFour = (uint32&)COMMAND_KEY[12]; + } + + uint32 sum = 0xC6EF3720; + uint32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) + { + dataTwo -= (((dataOne << 4) + keyThree) ^ (dataOne + sum) ^ ((dataOne >> 5) + keyFour)); + dataOne -= (((dataTwo << 4) + keyOne) ^ (dataTwo + sum) ^ ((dataTwo >> 5) + keyTwo)); + sum -= delta; + } + + cemu_assert(sum == 0); + + std::array decrypted = {uint8(dataOne & 0xFF), uint8((dataOne >> 8) & 0xFF), + uint8((dataOne >> 16) & 0xFF), uint8((dataOne >> 24) & 0xFF), + uint8(dataTwo & 0xFF), uint8((dataTwo >> 8) & 0xFF), + uint8((dataTwo >> 16) & 0xFF), uint8((dataTwo >> 24) & 0xFF)}; + return decrypted; + } + std::array DimensionsUSB::Encrypt(std::span buf, std::optional> key) + { + // Value to encrypt is separated in to two little endian 32 bit unsigned integers + uint32 dataOne = (uint32&)buf[0]; + uint32 dataTwo = (uint32&)buf[4]; + + // Use the key as 4 32 bit little endian unsigned integers + uint32 keyOne; + uint32 keyTwo; + uint32 keyThree; + uint32 keyFour; + + if (key) + { + keyOne = (uint32&)key.value()[0]; + keyTwo = (uint32&)key.value()[4]; + keyThree = (uint32&)key.value()[8]; + keyFour = (uint32&)key.value()[12]; + } + else + { + keyOne = (uint32&)COMMAND_KEY[0]; + keyTwo = (uint32&)COMMAND_KEY[4]; + keyThree = (uint32&)COMMAND_KEY[8]; + keyFour = (uint32&)COMMAND_KEY[12]; + } + + uint32 sum = 0; + uint32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) + { + sum += delta; + dataOne += (((dataTwo << 4) + keyOne) ^ (dataTwo + sum) ^ ((dataTwo >> 5) + keyTwo)); + dataTwo += (((dataOne << 4) + keyThree) ^ (dataOne + sum) ^ ((dataOne >> 5) + keyFour)); + } + + cemu_assert(sum == 0xC6EF3720); + + std::array encrypted = {uint8(dataOne & 0xFF), uint8((dataOne >> 8) & 0xFF), + uint8((dataOne >> 16) & 0xFF), uint8((dataOne >> 24) & 0xFF), + uint8(dataTwo & 0xFF), uint8((dataTwo >> 8) & 0xFF), + uint8((dataTwo >> 16) & 0xFF), uint8((dataTwo >> 24) & 0xFF)}; + return encrypted; + } + + std::array DimensionsUSB::GenerateFigureKey(const std::array& buf) + { + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + uint32 scrambleA = Scramble(uid, 3); + uint32 scrambleB = Scramble(uid, 4); + uint32 scrambleC = Scramble(uid, 5); + uint32 scrambleD = Scramble(uid, 6); + + return {uint8((scrambleA >> 24) & 0xFF), uint8((scrambleA >> 16) & 0xFF), + uint8((scrambleA >> 8) & 0xFF), uint8(scrambleA & 0xFF), + uint8((scrambleB >> 24) & 0xFF), uint8((scrambleB >> 16) & 0xFF), + uint8((scrambleB >> 8) & 0xFF), uint8(scrambleB & 0xFF), + uint8((scrambleC >> 24) & 0xFF), uint8((scrambleC >> 16) & 0xFF), + uint8((scrambleC >> 8) & 0xFF), uint8(scrambleC & 0xFF), + uint8((scrambleD >> 24) & 0xFF), uint8((scrambleD >> 16) & 0xFF), + uint8((scrambleD >> 8) & 0xFF), uint8(scrambleD & 0xFF)}; + } + + uint32 DimensionsUSB::Scramble(const std::array& uid, uint8 count) + { + std::vector toScramble; + toScramble.reserve(uid.size() + CHAR_CONSTANT.size()); + for (uint8 x : uid) + { + toScramble.push_back(x); + } + for (uint8 c : CHAR_CONSTANT) + { + toScramble.push_back(c); + } + toScramble[(count * 4) - 1] = 0xaa; + + std::array randomized = DimensionsRandomize(toScramble, count); + + return (uint32be&)randomized[0]; + } + + std::array DimensionsUSB::PWDGenerate(const std::array& buf) + { + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + std::vector pwdCalc = {PWD_CONSTANT.begin(), PWD_CONSTANT.end() - 1}; + for (uint8 i = 0; i < uid.size(); i++) + { + pwdCalc.insert(pwdCalc.begin() + i, uid[i]); + } + + return DimensionsRandomize(pwdCalc, 8); + } + + std::array DimensionsUSB::DimensionsRandomize(const std::vector key, uint8 count) + { + uint32 scrambled = 0; + for (uint8 i = 0; i < count; i++) + { + const uint32 v4 = std::rotr(scrambled, 25); + const uint32 v5 = std::rotr(scrambled, 10); + const uint32 b = (uint32&)key[i * 4]; + scrambled = b + v4 + v5 - scrambled; + } + return {uint8(scrambled & 0xFF), uint8(scrambled >> 8 & 0xFF), uint8(scrambled >> 16 & 0xFF), uint8(scrambled >> 24 & 0xFF)}; + } + + uint32 DimensionsUSB::GetFigureId(const std::array& buf) + { + const std::array figureKey = GenerateFigureKey(buf); + + const std::span modelNumber = std::span{buf.begin() + (36 * 4), 8}; + + const std::array decrypted = Decrypt(modelNumber, figureKey); + + const uint32 figNum = (uint32&)decrypted[0]; + // Characters have their model number encrypted in page 36 + if (figNum < 1000) + { + return figNum; + } + // Vehicles/Gadgets have their model number written as little endian in page 36 + return (uint32&)modelNumber[0]; + } + + DimensionsUSB::DimensionsMini& + DimensionsUSB::GetFigureByIndex(uint8 index) + { + return m_figures[index]; + } + + void DimensionsUSB::QueryBlock(uint8 index, uint8 page, + std::array& replyBuf, + uint8 sequence) + { + std::lock_guard lock(m_dimensionsMutex); + + replyBuf[0] = 0x55; + replyBuf[1] = 0x12; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + + // Index from game begins at 1 rather than 0, so minus 1 here + if (const uint8 figureIndex = index - 1; figureIndex < 7) + { + const DimensionsMini& figure = GetFigureByIndex(figureIndex); + + // Query 4 pages of 4 bytes from the figure, copy this to the response + if (figure.index != 255 && (4 * page) < ((0x2D * 4) - 16)) + { + std::memcpy(&replyBuf[4], figure.data.data() + (4 * page), 16); + } + } + replyBuf[20] = GenerateChecksum(replyBuf, 20); + } + + void DimensionsUSB::WriteBlock(uint8 index, uint8 page, std::span toWriteBuf, + std::array& replyBuf, uint8 sequence) + { + std::lock_guard lock(m_dimensionsMutex); + + replyBuf[0] = 0x55; + replyBuf[1] = 0x02; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + + // Index from game begins at 1 rather than 0, so minus 1 here + if (const uint8 figureIndex = index - 1; figureIndex < 7) + { + DimensionsMini& figure = GetFigureByIndex(figureIndex); + + // Copy 4 bytes to the page on the figure requested by the game + if (figure.index != 255 && page < 0x2D) + { + // Id is written to page 36 + if (page == 36) + { + figure.id = (uint32&)toWriteBuf[0]; + } + std::memcpy(figure.data.data() + (page * 4), toWriteBuf.data(), 4); + figure.Save(); + } + } + replyBuf[4] = GenerateChecksum(replyBuf, 4); + } + + void DimensionsUSB::GetModel(std::span buf, uint8 sequence, + std::array& replyBuf) + { + // Decrypt payload to 8 byte array, byte 1 is the index, 4-7 are the confirmation + std::array value = Decrypt(buf, std::nullopt); + uint8 index = value[0]; + uint32 conf = (uint32be&)value[4]; + // Response is the figure's id (little endian) followed by the confirmation from payload + // Index from game begins at 1 rather than 0, so minus 1 here + std::array valueToEncrypt = {}; + if (const uint8 figureIndex = index - 1; figureIndex < 7) + { + const DimensionsMini& figure = GetFigureByIndex(figureIndex); + valueToEncrypt = {uint8(figure.id & 0xFF), uint8((figure.id >> 8) & 0xFF), + uint8((figure.id >> 16) & 0xFF), uint8((figure.id >> 24) & 0xFF), + value[4], value[5], value[6], value[7]}; + } + std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); + replyBuf[0] = 0x55; + replyBuf[1] = 0x0a; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + memcpy(&replyBuf[4], encrypted.data(), encrypted.size()); + replyBuf[12] = GenerateChecksum(replyBuf, 12); + } + + void DimensionsUSB::RandomUID(std::array& uid_buffer) + { + uid_buffer[0] = 0x04; + uid_buffer[7] = 0x80; + + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0, 255); + + uid_buffer[1] = dist(mt); + uid_buffer[2] = dist(mt); + uid_buffer[4] = dist(mt); + uid_buffer[5] = dist(mt); + uid_buffer[6] = dist(mt); + } + + uint8 DimensionsUSB::GenerateChecksum(const std::array& data, + int num_of_bytes) const + { + int checksum = 0; + for (int i = 0; i < num_of_bytes; i++) + { + checksum += data[i]; + } + return (checksum & 0xFF); + } + + void DimensionsUSB::DimensionsMini::Save() + { + if (!dimFile) + return; + + dimFile->SetPosition(0); + dimFile->writeData(data.data(), data.size()); + } + + std::map DimensionsUSB::GetListMinifigs() + { + return s_listMinis; + } + + std::map DimensionsUSB::GetListTokens() + { + return s_listTokens; + } + + std::string DimensionsUSB::FindFigure(uint32 figNum) + { + for (const auto& it : GetListMinifigs()) + { + if (it.first == figNum) + { + return it.second; + } + } + for (const auto& it : GetListTokens()) + { + if (it.first == figNum) + { + return it.second; + } + } + return fmt::format("Unknown ({})", figNum); + } +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Dimensions.h b/src/Cafe/OS/libs/nsyshid/Dimensions.h new file mode 100644 index 00000000..d5a2a529 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Dimensions.h @@ -0,0 +1,108 @@ +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +namespace nsyshid +{ + class DimensionsToypadDevice final : public Device + { + public: + DimensionsToypadDevice(); + ~DimensionsToypadDevice() = default; + + bool Open() override; + + void Close() override; + + bool IsOpened() override; + + ReadResult Read(ReadMessage* message) override; + + WriteResult Write(WriteMessage* message) override; + + bool GetDescriptor(uint8 descType, + uint8 descIndex, + uint8 lang, + uint8* output, + uint32 outputMaxLength) override; + + bool SetProtocol(uint8 ifIndex, uint8 protocol) override; + + bool SetReport(ReportMessage* message) override; + + private: + bool m_IsOpened; + }; + + class DimensionsUSB + { + public: + struct DimensionsMini final + { + std::unique_ptr dimFile; + std::array data{}; + uint8 index = 255; + uint8 pad = 255; + uint32 id = 0; + void Save(); + }; + + void SendCommand(std::span buf); + std::array GetStatus(); + + void GenerateRandomNumber(std::span buf, uint8 sequence, + std::array& replyBuf); + void InitializeRNG(uint32 seed); + void GetChallengeResponse(std::span buf, uint8 sequence, + std::array& replyBuf); + void QueryBlock(uint8 index, uint8 page, std::array& replyBuf, + uint8 sequence); + void WriteBlock(uint8 index, uint8 page, std::span toWriteBuf, std::array& replyBuf, + uint8 sequence); + void GetModel(std::span buf, uint8 sequence, + std::array& replyBuf); + + bool RemoveFigure(uint8 pad, uint8 index, bool fullRemove); + bool TempRemove(uint8 index); + bool CancelRemove(uint8 index); + uint32 LoadFigure(const std::array& buf, std::unique_ptr file, uint8 pad, uint8 index); + bool CreateFigure(fs::path pathName, uint32 id); + bool MoveFigure(uint8 pad, uint8 index, uint8 oldPad, uint8 oldIndex); + static std::map GetListMinifigs(); + static std::map GetListTokens(); + std::string FindFigure(uint32 figNum); + + protected: + std::mutex m_dimensionsMutex; + std::array m_figures{}; + + private: + void RandomUID(std::array& uidBuffer); + uint8 GenerateChecksum(const std::array& data, + int numOfBytes) const; + std::array Decrypt(std::span buf, std::optional> key); + std::array Encrypt(std::span buf, std::optional> key); + std::array GenerateFigureKey(const std::array& uid); + std::array PWDGenerate(const std::array& uid); + std::array DimensionsRandomize(const std::vector key, uint8 count); + uint32 GetFigureId(const std::array& buf); + uint32 Scramble(const std::array& uid, uint8 count); + uint32 GetNext(); + DimensionsMini& GetFigureByIndex(uint8 index); + + uint32 m_randomA; + uint32 m_randomB; + uint32 m_randomC; + uint32 m_randomD; + + bool m_isAwake = false; + + std::queue> m_figureAddedRemovedResponses; + std::queue> m_queries; + }; + extern DimensionsUSB g_dimensionstoypad; + +} // namespace nsyshid \ No newline at end of file diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index e7920e84..f5ee7ab4 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -346,6 +346,7 @@ void CemuConfig::Load(XMLConfigParser& parser) auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); emulated_usb_devices.emulate_infinity_base = usbdevices.get("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base); + emulated_usb_devices.emulate_dimensions_toypad = usbdevices.get("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad); } void CemuConfig::Save(XMLConfigParser& parser) @@ -545,6 +546,7 @@ void CemuConfig::Save(XMLConfigParser& parser) auto usbdevices = config.set("EmulatedUsbDevices"); usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); usbdevices.set("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base.GetValue()); + usbdevices.set("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad.GetValue()); } GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 2f22cd76..be131266 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -521,6 +521,7 @@ struct CemuConfig { ConfigValue emulate_skylander_portal{false}; ConfigValue emulate_infinity_base{false}; + ConfigValue emulate_dimensions_toypad{false}; }emulated_usb_devices{}; private: diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index 3a0f534a..c77ae081 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -1,4 +1,4 @@ -#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h" +#include "EmulatedUSBDeviceFrame.h" #include @@ -8,14 +8,17 @@ #include "util/helpers/helpers.h" #include "Cafe/OS/libs/nsyshid/nsyshid.h" +#include "Cafe/OS/libs/nsyshid/Dimensions.h" #include "Common/FileStream.h" #include #include +#include #include #include #include +#include #include #include #include @@ -29,7 +32,6 @@ #include #include "resource/embedded/resources.h" -#include "EmulatedUSBDeviceFrame.h" EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) : wxFrame(parent, wxID_ANY, _("Emulated USB Devices"), wxDefaultPosition, @@ -44,6 +46,7 @@ EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal")); notebook->AddPage(AddInfinityPage(notebook), _("Infinity Base")); + notebook->AddPage(AddDimensionsPage(notebook), _("Dimensions Toypad")); sizer->Add(notebook, 1, wxEXPAND | wxALL, 2); @@ -120,8 +123,52 @@ wxPanel* EmulatedUSBDeviceFrame::AddInfinityPage(wxNotebook* notebook) return panel; } -wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, - wxStaticBox* box) +wxPanel* EmulatedUSBDeviceFrame::AddDimensionsPage(wxNotebook* notebook) +{ + auto* panel = new wxPanel(notebook); + auto* panel_sizer = new wxBoxSizer(wxVERTICAL); + auto* box = new wxStaticBox(panel, wxID_ANY, _("Dimensions Manager")); + auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); + + auto* row = new wxBoxSizer(wxHORIZONTAL); + + m_emulateToypad = + new wxCheckBox(box, wxID_ANY, _("Emulate Dimensions Toypad")); + m_emulateToypad->SetValue( + GetConfig().emulated_usb_devices.emulate_dimensions_toypad); + m_emulateToypad->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + GetConfig().emulated_usb_devices.emulate_dimensions_toypad = + m_emulateToypad->IsChecked(); + g_config.Save(); + }); + row->Add(m_emulateToypad, 1, wxEXPAND | wxALL, 2); + box_sizer->Add(row, 1, wxEXPAND | wxALL, 2); + auto* top_row = new wxBoxSizer(wxHORIZONTAL); + auto* bottom_row = new wxBoxSizer(wxHORIZONTAL); + + auto* dummy = new wxStaticText(box, wxID_ANY, ""); + + top_row->Add(AddDimensionPanel(2, 0, box), 1, wxEXPAND | wxALL, 2); + top_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 2); + top_row->Add(AddDimensionPanel(1, 1, box), 1, wxEXPAND | wxALL, 2); + top_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 2); + top_row->Add(AddDimensionPanel(3, 2, box), 1, wxEXPAND | wxALL, 2); + + bottom_row->Add(AddDimensionPanel(2, 3, box), 1, wxEXPAND | wxALL, 2); + bottom_row->Add(AddDimensionPanel(2, 4, box), 1, wxEXPAND | wxALL, 2); + bottom_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 0); + bottom_row->Add(AddDimensionPanel(3, 5, box), 1, wxEXPAND | wxALL, 2); + bottom_row->Add(AddDimensionPanel(3, 6, box), 1, wxEXPAND | wxALL, 2); + + box_sizer->Add(top_row, 1, wxEXPAND | wxALL, 2); + box_sizer->Add(bottom_row, 1, wxEXPAND | wxALL, 2); + panel_sizer->Add(box_sizer, 1, wxEXPAND | wxALL, 2); + panel->SetSizerAndFit(panel_sizer); + + return panel; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, wxStaticBox* box) { auto* row = new wxBoxSizer(wxHORIZONTAL); @@ -184,6 +231,44 @@ wxBoxSizer* EmulatedUSBDeviceFrame::AddInfinityRow(wxString name, uint8 rowNumbe return row; } +wxBoxSizer* EmulatedUSBDeviceFrame::AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box) +{ + auto* panel = new wxBoxSizer(wxVERTICAL); + + auto* combo_row = new wxBoxSizer(wxHORIZONTAL); + m_dimensionSlots[index] = new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, + wxTE_READONLY); + combo_row->Add(m_dimensionSlots[index], 1, wxEXPAND | wxALL, 2); + auto* move_button = new wxButton(box, wxID_ANY, _("Move")); + move_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + MoveMinifig(pad, index); + }); + + combo_row->Add(move_button, 1, wxEXPAND | wxALL, 2); + + auto* button_row = new wxBoxSizer(wxHORIZONTAL); + auto* load_button = new wxButton(box, wxID_ANY, _("Load")); + load_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + LoadMinifig(pad, index); + }); + auto* clear_button = new wxButton(box, wxID_ANY, _("Clear")); + clear_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + ClearMinifig(pad, index); + }); + auto* create_button = new wxButton(box, wxID_ANY, _("Create")); + create_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + CreateMinifig(pad, index); + }); + button_row->Add(clear_button, 1, wxEXPAND | wxALL, 2); + button_row->Add(create_button, 1, wxEXPAND | wxALL, 2); + button_row->Add(load_button, 1, wxEXPAND | wxALL, 2); + + panel->Add(combo_row, 1, wxEXPAND | wxALL, 2); + panel->Add(button_row, 1, wxEXPAND | wxALL, 2); + + return panel; +} + void EmulatedUSBDeviceFrame::LoadSkylander(uint8 slot) { wxFileDialog openFileDialog(this, _("Open Skylander dump"), "", "", @@ -307,8 +392,8 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) return; m_filePath = saveFileDialog.GetPath(); - - if(!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar)) + + if (!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar)) { wxMessageDialog errorMessage(this, "Failed to create file"); errorMessage.ShowModal(); @@ -351,6 +436,80 @@ wxString CreateSkylanderDialog::GetFilePath() const return m_filePath; } +void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() +{ + for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) + { + std::string displayString; + if (auto sd = m_skySlots[i]) + { + auto [portalSlot, skyId, skyVar] = sd.value(); + displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar); + } + else + { + displayString = "None"; + } + + m_skylanderSlots[i]->ChangeValue(displayString); + } +} + +void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot) +{ + wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "", + "BIN files (*.bin)|*.bin", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) + { + wxMessageDialog errorMessage(this, "File Okay Error"); + errorMessage.ShowModal(); + return; + } + + LoadFigurePath(slot, openFileDialog.GetPath()); +} + +void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path) +{ + std::unique_ptr infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); + if (!infFile) + { + wxMessageDialog errorMessage(this, "File Open Error"); + errorMessage.ShowModal(); + return; + } + + std::array fileData; + if (infFile->readData(fileData.data(), fileData.size()) != fileData.size()) + { + wxMessageDialog open_error(this, "Failed to read file! File was too small"); + open_error.ShowModal(); + return; + } + ClearFigure(slot); + + uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot); + m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second); +} + +void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot) +{ + cemuLog_log(LogType::Force, "Create Figure: {}", slot); + CreateInfinityFigureDialog create_dlg(this, slot); + create_dlg.ShowModal(); + if (create_dlg.GetReturnCode() == 1) + { + LoadFigurePath(slot, create_dlg.GetFilePath()); + } +} + +void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot) +{ + m_infinitySlots[slot]->ChangeValue("None"); + nsyshid::g_infinitybase.RemoveFigure(slot); +} + CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 slot) : wxDialog(parent, wxID_ANY, _("Infinity Figure Creator"), wxDefaultPosition, wxSize(500, 150)) { @@ -447,76 +606,231 @@ wxString CreateInfinityFigureDialog::GetFilePath() const return m_filePath; } -void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot) +void EmulatedUSBDeviceFrame::LoadMinifig(uint8 pad, uint8 index) { - wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "", - "BIN files (*.bin)|*.bin", + wxFileDialog openFileDialog(this, _("Load Dimensions Figure"), "", "", + "Dimensions files (*.bin)|*.bin", wxFD_OPEN | wxFD_FILE_MUST_EXIST); if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) + return; + + LoadMinifigPath(openFileDialog.GetPath(), pad, index); +} + +void EmulatedUSBDeviceFrame::LoadMinifigPath(wxString path_name, uint8 pad, uint8 index) +{ + std::unique_ptr dim_file(FileStream::openFile2(_utf8ToPath(path_name.utf8_string()), true)); + if (!dim_file) { - wxMessageDialog errorMessage(this, "File Okay Error"); + wxMessageDialog errorMessage(this, "Failed to open minifig file"); errorMessage.ShowModal(); return; } - LoadFigurePath(slot, openFileDialog.GetPath()); -} + std::array file_data; -void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path) -{ - std::unique_ptr infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); - if (!infFile) + if (dim_file->readData(file_data.data(), file_data.size()) != file_data.size()) { - wxMessageDialog errorMessage(this, "File Open Error"); + wxMessageDialog errorMessage(this, "Failed to read minifig file data"); errorMessage.ShowModal(); return; } - std::array fileData; - if (infFile->readData(fileData.data(), fileData.size()) != fileData.size()) - { - wxMessageDialog open_error(this, "Failed to read file! File was too small"); - open_error.ShowModal(); - return; - } - ClearFigure(slot); + ClearMinifig(pad, index); - uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot); - m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second); + uint32 id = nsyshid::g_dimensionstoypad.LoadFigure(file_data, std::move(dim_file), pad, index); + m_dimensionSlots[index]->ChangeValue(nsyshid::g_dimensionstoypad.FindFigure(id)); + m_dimSlots[index] = id; } -void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot) +void EmulatedUSBDeviceFrame::ClearMinifig(uint8 pad, uint8 index) { - cemuLog_log(LogType::Force, "Create Figure: {}", slot); - CreateInfinityFigureDialog create_dlg(this, slot); + nsyshid::g_dimensionstoypad.RemoveFigure(pad, index, true); + m_dimensionSlots[index]->ChangeValue("None"); + m_dimSlots[index] = std::nullopt; +} + +void EmulatedUSBDeviceFrame::CreateMinifig(uint8 pad, uint8 index) +{ + CreateDimensionFigureDialog create_dlg(this); create_dlg.ShowModal(); if (create_dlg.GetReturnCode() == 1) { - LoadFigurePath(slot, create_dlg.GetFilePath()); + LoadMinifigPath(create_dlg.GetFilePath(), pad, index); } } -void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot) +void EmulatedUSBDeviceFrame::MoveMinifig(uint8 pad, uint8 index) { - m_infinitySlots[slot]->ChangeValue("None"); - nsyshid::g_infinitybase.RemoveFigure(slot); -} + if (!m_dimSlots[index]) + return; -void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() -{ - for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) + MoveDimensionFigureDialog move_dlg(this, index); + nsyshid::g_dimensionstoypad.TempRemove(index); + move_dlg.ShowModal(); + if (move_dlg.GetReturnCode() == 1) { - std::string displayString; - if (auto sd = m_skySlots[i]) + nsyshid::g_dimensionstoypad.MoveFigure(move_dlg.GetNewPad(), move_dlg.GetNewIndex(), pad, index); + if (index != move_dlg.GetNewIndex()) { - auto [portalSlot, skyId, skyVar] = sd.value(); - displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar); + m_dimSlots[move_dlg.GetNewIndex()] = m_dimSlots[index]; + m_dimensionSlots[move_dlg.GetNewIndex()]->ChangeValue(m_dimensionSlots[index]->GetValue()); + m_dimSlots[index] = std::nullopt; + m_dimensionSlots[index]->ChangeValue("None"); } - else - { - displayString = "None"; - } - - m_skylanderSlots[i]->ChangeValue(displayString); } + else + { + nsyshid::g_dimensionstoypad.CancelRemove(index); + } +} + +CreateDimensionFigureDialog::CreateDimensionFigureDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Dimensions Figure Creator"), wxDefaultPosition, wxSize(500, 200)) +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* comboRow = new wxBoxSizer(wxHORIZONTAL); + + auto* comboBox = new wxComboBox(this, wxID_ANY); + comboBox->Append("---Select---", reinterpret_cast(0xFFFFFFFF)); + wxArrayString filterlist; + for (const auto& it : nsyshid::g_dimensionstoypad.GetListMinifigs()) + { + const uint32 figure = it.first; + comboBox->Append(it.second, reinterpret_cast(figure)); + filterlist.Add(it.second); + } + comboBox->SetSelection(0); + bool enabled = comboBox->AutoComplete(filterlist); + comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); + + auto* figNumRow = new wxBoxSizer(wxHORIZONTAL); + + wxIntegerValidator validator; + + auto* labelFigNum = new wxStaticText(this, wxID_ANY, "Figure Number:"); + auto* editFigNum = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); + + figNumRow->Add(labelFigNum, 1, wxALL, 5); + figNumRow->Add(editFigNum, 1, wxALL, 5); + + auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); + + auto* createButton = new wxButton(this, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [editFigNum, this](wxCommandEvent&) { + long longFigNum; + if (!editFigNum->GetValue().ToLong(&longFigNum) || longFigNum > 0xFFFF) + { + wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); + idError.ShowModal(); + this->EndModal(0); + } + uint16 figNum = longFigNum & 0xFFFF; + auto figure = nsyshid::g_dimensionstoypad.FindFigure(figNum); + wxString predefName = figure + ".bin"; + wxFileDialog + saveFileDialog(this, _("Create Dimensions Figure file"), "", predefName, + "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (saveFileDialog.ShowModal() == wxID_CANCEL) + this->EndModal(0); + + m_filePath = saveFileDialog.GetPath(); + + nsyshid::g_dimensionstoypad.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum); + + this->EndModal(1); + }); + auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); + cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + this->EndModal(0); + }); + + comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editFigNum, this](wxCommandEvent&) { + const uint64 fig_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); + if (fig_info != 0xFFFF) + { + const uint16 figNum = fig_info & 0xFFFF; + + editFigNum->SetValue(wxString::Format(wxT("%i"), figNum)); + } + }); + + buttonRow->Add(createButton, 1, wxALL, 5); + buttonRow->Add(cancelButton, 1, wxALL, 5); + + sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(figNumRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); + + this->SetSizer(sizer); + this->Centre(wxBOTH); +} + +wxString CreateDimensionFigureDialog::GetFilePath() const +{ + return m_filePath; +} + +MoveDimensionFigureDialog::MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex) + : wxDialog(parent, wxID_ANY, _("Dimensions Figure Mover"), wxDefaultPosition, wxSize(700, 300)) +{ + auto* sizer = new wxGridSizer(2, 5, 10, 10); + + std::array, 7> ids = parent->GetCurrentMinifigs(); + + sizer->Add(AddMinifigSlot(2, 0, currentIndex, ids[0]), 1, wxALL, 5); + sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(1, 1, currentIndex, ids[1]), 1, wxALL, 5); + sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(3, 2, currentIndex, ids[2]), 1, wxALL, 5); + + sizer->Add(AddMinifigSlot(2, 3, currentIndex, ids[3]), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(2, 4, currentIndex, ids[4]), 1, wxALL, 5); + sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(3, 5, currentIndex, ids[5]), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(3, 6, currentIndex, ids[6]), 1, wxALL, 5); + + this->SetSizer(sizer); + this->Centre(wxBOTH); +} + +wxBoxSizer* MoveDimensionFigureDialog::AddMinifigSlot(uint8 pad, uint8 index, uint8 currentIndex, std::optional currentId) +{ + auto* panel = new wxBoxSizer(wxVERTICAL); + + auto* label = new wxStaticText(this, wxID_ANY, "None"); + if (currentId) + label->SetLabel(nsyshid::g_dimensionstoypad.FindFigure(currentId.value())); + + auto* moveButton = new wxButton(this, wxID_ANY, _("Move Here")); + if (index == currentIndex) + moveButton->SetLabelText("Pick up and Place"); + + moveButton->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + m_newPad = pad; + m_newIndex = index; + this->EndModal(1); + }); + + panel->Add(label, 1, wxALL, 5); + panel->Add(moveButton, 1, wxALL, 5); + + return panel; +} + +uint8 MoveDimensionFigureDialog::GetNewPad() const +{ + return m_newPad; +} + +uint8 MoveDimensionFigureDialog::GetNewIndex() const +{ + return m_newIndex; +} + +std::array, 7> EmulatedUSBDeviceFrame::GetCurrentMinifigs() +{ + return m_dimSlots; } \ No newline at end of file diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h index ae29a036..78c70a4a 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h @@ -17,33 +17,47 @@ class wxStaticBox; class wxString; class wxTextCtrl; -class EmulatedUSBDeviceFrame : public wxFrame { +class EmulatedUSBDeviceFrame : public wxFrame +{ public: EmulatedUSBDeviceFrame(wxWindow* parent); ~EmulatedUSBDeviceFrame(); + std::array, 7> GetCurrentMinifigs(); private: wxCheckBox* m_emulatePortal; wxCheckBox* m_emulateBase; + wxCheckBox* m_emulateToypad; std::array m_skylanderSlots; std::array m_infinitySlots; + std::array m_dimensionSlots; std::array>, nsyshid::MAX_SKYLANDERS> m_skySlots; + std::array, 7> m_dimSlots; wxPanel* AddSkylanderPage(wxNotebook* notebook); wxPanel* AddInfinityPage(wxNotebook* notebook); + wxPanel* AddDimensionsPage(wxNotebook* notebook); wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); wxBoxSizer* AddInfinityRow(wxString name, uint8 row_number, wxStaticBox* box); + wxBoxSizer* AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box); void LoadSkylander(uint8 slot); void LoadSkylanderPath(uint8 slot, wxString path); void CreateSkylander(uint8 slot); void ClearSkylander(uint8 slot); + void UpdateSkylanderEdits(); void LoadFigure(uint8 slot); void LoadFigurePath(uint8 slot, wxString path); void CreateFigure(uint8 slot); void ClearFigure(uint8 slot); - void UpdateSkylanderEdits(); + void LoadMinifig(uint8 pad, uint8 index); + void LoadMinifigPath(wxString path_name, uint8 pad, uint8 index); + void CreateMinifig(uint8 pad, uint8 index); + void ClearMinifig(uint8 pad, uint8 index); + void MoveMinifig(uint8 pad, uint8 index); }; -class CreateSkylanderDialog : public wxDialog { + +class CreateSkylanderDialog : public wxDialog +{ public: explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot); wxString GetFilePath() const; @@ -52,11 +66,37 @@ class CreateSkylanderDialog : public wxDialog { wxString m_filePath; }; -class CreateInfinityFigureDialog : public wxDialog { +class CreateInfinityFigureDialog : public wxDialog +{ public: explicit CreateInfinityFigureDialog(wxWindow* parent, uint8 slot); wxString GetFilePath() const; protected: wxString m_filePath; +}; + +class CreateDimensionFigureDialog : public wxDialog +{ + public: + explicit CreateDimensionFigureDialog(wxWindow* parent); + wxString GetFilePath() const; + + protected: + wxString m_filePath; +}; + +class MoveDimensionFigureDialog : public wxDialog +{ + public: + explicit MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex); + uint8 GetNewPad() const; + uint8 GetNewIndex() const; + + protected: + uint8 m_newIndex = 0; + uint8 m_newPad = 0; + + private: + wxBoxSizer* AddMinifigSlot(uint8 pad, uint8 index, uint8 oldIndex, std::optional currentId); }; \ No newline at end of file From a5717e1b11fe2a6c2c1be0afbd4908d765a777af Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 8 Nov 2024 01:07:53 +0100 Subject: [PATCH 037/137] FST: Refactoring to fix a read bug + verify all reads - Fixes a bug where corrupted data would be returned when reading files from unhashed sections with non-block aligned offset or size - Added hash checks for all reads where possible. This means that FST now can automatically catch corruptions when they are encountered while reading from the volume --- src/Cafe/Filesystem/FST/FST.cpp | 379 ++++++++++++++++++++++---------- src/Cafe/Filesystem/FST/FST.h | 38 +++- 2 files changed, 301 insertions(+), 116 deletions(-) diff --git a/src/Cafe/Filesystem/FST/FST.cpp b/src/Cafe/Filesystem/FST/FST.cpp index 570671d4..f1255778 100644 --- a/src/Cafe/Filesystem/FST/FST.cpp +++ b/src/Cafe/Filesystem/FST/FST.cpp @@ -3,8 +3,7 @@ #include "Cemu/ncrypto/ncrypto.h" #include "Cafe/Filesystem/WUD/wud.h" #include "util/crypto/aes128.h" -#include "openssl/evp.h" /* EVP_Digest */ -#include "openssl/sha.h" /* SHA1 / SHA256_DIGEST_LENGTH */ +#include "openssl/sha.h" /* SHA1 / SHA256 */ #include "fstUtil.h" #include "FST.h" @@ -141,7 +140,7 @@ struct DiscPartitionTableHeader static constexpr uint32 MAGIC_VALUE = 0xCCA6E67B; /* +0x00 */ uint32be magic; - /* +0x04 */ uint32be sectorSize; // must be 0x8000? + /* +0x04 */ uint32be blockSize; // must be 0x8000? /* +0x08 */ uint8 partitionTableHash[20]; // hash of the data range at +0x800 to end of sector (0x8000) /* +0x1C */ uint32be numPartitions; }; @@ -164,10 +163,10 @@ struct DiscPartitionHeader static constexpr uint32 MAGIC_VALUE = 0xCC93A4F5; /* +0x00 */ uint32be magic; - /* +0x04 */ uint32be sectorSize; // must match DISC_SECTOR_SIZE + /* +0x04 */ uint32be sectorSize; // must match DISC_SECTOR_SIZE for hashed blocks /* +0x08 */ uint32be ukn008; - /* +0x0C */ uint32be ukn00C; + /* +0x0C */ uint32be ukn00C; // h3 array size? /* +0x10 */ uint32be h3HashNum; /* +0x14 */ uint32be fstSize; // in bytes /* +0x18 */ uint32be fstSector; // relative to partition start @@ -178,13 +177,15 @@ struct DiscPartitionHeader /* +0x24 */ uint8 fstHashType; /* +0x25 */ uint8 fstEncryptionType; // purpose of this isn't really understood. Maybe it controls which key is being used? (1 -> disc key, 2 -> partition key) - /* +0x26 */ uint8 versionA; - /* +0x27 */ uint8 ukn027; // also a version field? + /* +0x26 */ uint8be versionA; + /* +0x27 */ uint8be ukn027; // also a version field? // there is an array at +0x40 ? Related to H3 list. Also related to value at +0x0C and h3HashNum + /* +0x28 */ uint8be _uknOrPadding028[0x18]; + /* +0x40 */ uint8be h3HashArray[32]; // dynamic size. Only present if fstHashType != 0 }; -static_assert(sizeof(DiscPartitionHeader) == 0x28); +static_assert(sizeof(DiscPartitionHeader) == 0x40+0x20); bool FSTVolume::FindDiscKey(const fs::path& path, NCrypto::AesKey& discTitleKey) { @@ -269,7 +270,7 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d cemuLog_log(LogType::Force, "Disc image rejected because decryption failed"); return nullptr; } - if (partitionHeader->sectorSize != DISC_SECTOR_SIZE) + if (partitionHeader->blockSize != DISC_SECTOR_SIZE) { cemuLog_log(LogType::Force, "Disc image rejected because partition sector size is invalid"); return nullptr; @@ -336,6 +337,9 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d cemu_assert_debug(partitionHeaderSI.fstEncryptionType == 1); // todo - check other fields? + if(partitionHeaderSI.fstHashType == 0 && partitionHeaderSI.h3HashNum != 0) + cemuLog_log(LogType::Force, "FST: Partition uses unhashed blocks but stores a non-zero amount of H3 hashes"); + // GM partition DiscPartitionHeader partitionHeaderGM{}; if (!readPartitionHeader(partitionHeaderGM, gmPartitionIndex)) @@ -349,9 +353,10 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d // if decryption is necessary // load SI FST dataSource->SetBaseOffset((uint64)partitionArray[siPartitionIndex].partitionAddress * DISC_SECTOR_SIZE); - auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast(partitionHeaderSI.fstHashType)); + auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast(partitionHeaderSI.fstHashType), nullptr); if (!siFST) return nullptr; + cemu_assert_debug(!(siFST->HashIsDisabled() && partitionHeaderSI.h3HashNum != 0)); // if hash is disabled, no H3 data may be present // load ticket file for partition that we want to decrypt NCrypto::ETicketParser ticketParser; std::vector ticketData = siFST->ExtractFile(fmt::format("{:02x}/title.tik", gmPartitionIndex)); @@ -360,16 +365,32 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d cemuLog_log(LogType::Force, "Disc image ticket file is invalid"); return nullptr; } +#if 0 + // each SI partition seems to contain a title.tmd that we could parse and which should have information about the associated GM partition + // but the console seems to ignore this file for disc images, at least when mounting, so we shouldn't rely on it either + std::vector tmdData = siFST->ExtractFile(fmt::format("{:02x}/title.tmd", gmPartitionIndex)); + if (tmdData.empty()) + { + cemuLog_log(LogType::Force, "Disc image TMD file is missing"); + return nullptr; + } + // parse TMD + NCrypto::TMDParser tmdParser; + if (!tmdParser.parse(tmdData.data(), tmdData.size())) + { + cemuLog_log(LogType::Force, "Disc image TMD file is invalid"); + return nullptr; + } +#endif delete siFST; - NCrypto::AesKey gmTitleKey; ticketParser.GetTitleKey(gmTitleKey); - // load GM partition dataSource->SetBaseOffset((uint64)partitionArray[gmPartitionIndex].partitionAddress * DISC_SECTOR_SIZE); - FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast(partitionHeaderGM.fstHashType)); + FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast(partitionHeaderGM.fstHashType), nullptr); if (r) SET_FST_ERROR(OK); + cemu_assert_debug(!(r->HashIsDisabled() && partitionHeaderGM.h3HashNum != 0)); // if hash is disabled, no H3 data may be present return r; } @@ -426,15 +447,15 @@ FSTVolume* FSTVolume::OpenFromContentFolder(fs::path folderPath, ErrorCode* erro } // load FST // fstSize = size of first cluster? - FSTVolume* fstVolume = FSTVolume::OpenFST(std::move(dataSource), 0, fstSize, &titleKey, fstHashMode); + FSTVolume* fstVolume = FSTVolume::OpenFST(std::move(dataSource), 0, fstSize, &titleKey, fstHashMode, &tmdParser); if (fstVolume) SET_FST_ERROR(OK); return fstVolume; } -FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode) +FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD) { - cemu_assert_debug(fstHashMode != ClusterHashMode::RAW || fstHashMode != ClusterHashMode::RAW2); + cemu_assert_debug(fstHashMode != ClusterHashMode::RAW || fstHashMode != ClusterHashMode::RAW_STREAM); if (fstSize < sizeof(FSTHeader)) return nullptr; constexpr uint64 FST_CLUSTER_OFFSET = 0; @@ -465,6 +486,34 @@ FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint3 clusterTable[i].offset = clusterDataTable[i].offset; clusterTable[i].size = clusterDataTable[i].size; clusterTable[i].hashMode = static_cast((uint8)clusterDataTable[i].hashMode); + clusterTable[i].hasContentHash = false; // from the TMD file (H4?) + } + // if the TMD is available (when opening .app files) we can use the extra info from it to validate unhashed clusters + // each content entry in the TMD corresponds to one cluster used by the FST + if(optionalTMD) + { + if(numCluster != optionalTMD->GetContentList().size()) + { + cemuLog_log(LogType::Force, "FST: Number of clusters does not match TMD content list"); + return nullptr; + } + auto& contentList = optionalTMD->GetContentList(); + for(size_t i=0; im_offsetFactor = fstHeader->offsetFactor; fstVolume->m_sectorSize = DISC_SECTOR_SIZE; fstVolume->m_partitionTitlekey = *partitionTitleKey; - std::swap(fstVolume->m_cluster, clusterTable); - std::swap(fstVolume->m_entries, fstEntries); - std::swap(fstVolume->m_nameStringTable, nameStringTable); + fstVolume->m_hashIsDisabled = fstHeader->hashIsDisabled != 0; + fstVolume->m_cluster = std::move(clusterTable); + fstVolume->m_entries = std::move(fstEntries); + fstVolume->m_nameStringTable = std::move(nameStringTable); return fstVolume; } -FSTVolume* FSTVolume::OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode) +FSTVolume* FSTVolume::OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD) { FSTDataSource* ds = dataSource.release(); - FSTVolume* fstVolume = OpenFST(ds, fstOffset, fstSize, partitionTitleKey, fstHashMode); + FSTVolume* fstVolume = OpenFST(ds, fstOffset, fstSize, partitionTitleKey, fstHashMode, optionalTMD); if (!fstVolume) { delete ds; @@ -757,7 +807,7 @@ uint32 FSTVolume::ReadFile(FSTFileHandle& fileHandle, uint32 offset, uint32 size return 0; cemu_assert_debug(!HAS_FLAG(entry.GetFlags(), FSTEntry::FLAGS::FLAG_LINK)); FSTCluster& cluster = m_cluster[entry.fileInfo.clusterIndex]; - if (cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW2) + if (cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW_STREAM) return ReadFile_HashModeRaw(entry.fileInfo.clusterIndex, entry, offset, size, dataOut); else if (cluster.hashMode == ClusterHashMode::HASH_INTERLEAVED) return ReadFile_HashModeHashed(entry.fileInfo.clusterIndex, entry, offset, size, dataOut); @@ -765,87 +815,15 @@ uint32 FSTVolume::ReadFile(FSTFileHandle& fileHandle, uint32 offset, uint32 size return 0; } -uint32 FSTVolume::ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut) -{ - const uint32 readSizeInput = readSize; - uint8* dataOutU8 = (uint8*)dataOut; - if (readOffset >= entry.fileInfo.fileSize) - return 0; - else if ((readOffset + readSize) >= entry.fileInfo.fileSize) - readSize = (entry.fileInfo.fileSize - readOffset); - - const FSTCluster& cluster = m_cluster[clusterIndex]; - uint64 clusterOffset = (uint64)cluster.offset * m_sectorSize; - uint64 absFileOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset; - - // make sure the raw range we read is aligned to AES block size (16) - uint64 readAddrStart = absFileOffset & ~0xF; - uint64 readAddrEnd = (absFileOffset + readSize + 0xF) & ~0xF; - - bool usesInitialIV = readOffset < 16; - if (!usesInitialIV) - readAddrStart -= 16; // read previous AES block since we require it for the IV - uint32 prePadding = (uint32)(absFileOffset - readAddrStart); // number of extra bytes we read before readOffset (for AES alignment and IV calculation) - uint32 postPadding = (uint32)(readAddrEnd - (absFileOffset + readSize)); - - uint8 readBuffer[64 * 1024]; - // read first chunk - // if file read offset (readOffset) is within the first AES-block then use initial IV calculated from cluster index - // otherwise read previous AES-block is the IV (AES-CBC) - uint64 readAddrCurrent = readAddrStart; - uint32 rawBytesToRead = (uint32)std::min((readAddrEnd - readAddrStart), (uint64)sizeof(readBuffer)); - if (m_dataSource->readData(clusterIndex, clusterOffset, readAddrCurrent, readBuffer, rawBytesToRead) != rawBytesToRead) - { - cemuLog_log(LogType::Force, "FST read error in raw content"); - return 0; - } - readAddrCurrent += rawBytesToRead; - - uint8 iv[16]{}; - if (usesInitialIV) - { - // for the first AES block, the IV is initialized from cluster index - iv[0] = (uint8)(clusterIndex >> 8); - iv[1] = (uint8)(clusterIndex >> 0); - AES128_CBC_decrypt_updateIV(readBuffer, readBuffer, rawBytesToRead, m_partitionTitlekey.b, iv); - std::memcpy(dataOutU8, readBuffer + prePadding, rawBytesToRead - prePadding - postPadding); - dataOutU8 += (rawBytesToRead - prePadding - postPadding); - readSize -= (rawBytesToRead - prePadding - postPadding); - } - else - { - // IV is initialized from previous AES block (AES-CBC) - std::memcpy(iv, readBuffer, 16); - AES128_CBC_decrypt_updateIV(readBuffer + 16, readBuffer + 16, rawBytesToRead - 16, m_partitionTitlekey.b, iv); - std::memcpy(dataOutU8, readBuffer + prePadding, rawBytesToRead - prePadding - postPadding); - dataOutU8 += (rawBytesToRead - prePadding - postPadding); - readSize -= (rawBytesToRead - prePadding - postPadding); - } - - // read remaining chunks - while (readSize > 0) - { - uint32 bytesToRead = (uint32)std::min((uint32)sizeof(readBuffer), readSize); - uint32 alignedBytesToRead = (bytesToRead + 15) & ~0xF; - if (m_dataSource->readData(clusterIndex, clusterOffset, readAddrCurrent, readBuffer, alignedBytesToRead) != alignedBytesToRead) - { - cemuLog_log(LogType::Force, "FST read error in raw content"); - return 0; - } - AES128_CBC_decrypt_updateIV(readBuffer, readBuffer, alignedBytesToRead, m_partitionTitlekey.b, iv); - std::memcpy(dataOutU8, readBuffer, bytesToRead); - dataOutU8 += bytesToRead; - readSize -= bytesToRead; - readAddrCurrent += alignedBytesToRead; - } - - return readSizeInput - readSize; -} - constexpr size_t BLOCK_SIZE = 0x10000; constexpr size_t BLOCK_HASH_SIZE = 0x0400; constexpr size_t BLOCK_FILE_SIZE = 0xFC00; +struct FSTRawBlock +{ + std::vector rawData; // unhashed block size depends on sector size field in partition header +}; + struct FSTHashedBlock { uint8 rawData[BLOCK_SIZE]; @@ -887,12 +865,160 @@ struct FSTHashedBlock static_assert(sizeof(FSTHashedBlock) == BLOCK_SIZE); +struct FSTCachedRawBlock +{ + FSTRawBlock blockData; + uint8 ivForNextBlock[16]; + uint64 lastAccess; +}; + struct FSTCachedHashedBlock { FSTHashedBlock blockData; uint64 lastAccess; }; +// Checks cache fill state and if necessary drops least recently accessed block from the cache. Optionally allows to recycle the released cache entry to cut down cost of memory allocation and clearing +void FSTVolume::TrimCacheIfRequired(FSTCachedRawBlock** droppedRawBlock, FSTCachedHashedBlock** droppedHashedBlock) +{ + // calculate size used by cache + size_t cacheSize = 0; + for (auto& itr : m_cacheDecryptedRawBlocks) + cacheSize += itr.second->blockData.rawData.size(); + for (auto& itr : m_cacheDecryptedHashedBlocks) + cacheSize += sizeof(FSTCachedHashedBlock) + sizeof(FSTHashedBlock); + // only trim if cache is full (larger than 2MB) + if (cacheSize < 2*1024*1024) // 2MB + return; + // scan both cache lists to find least recently accessed block to drop + auto dropRawItr = std::min_element(m_cacheDecryptedRawBlocks.begin(), m_cacheDecryptedRawBlocks.end(), [](const auto& a, const auto& b) -> bool + { return a.second->lastAccess < b.second->lastAccess; }); + auto dropHashedItr = std::min_element(m_cacheDecryptedHashedBlocks.begin(), m_cacheDecryptedHashedBlocks.end(), [](const auto& a, const auto& b) -> bool + { return a.second->lastAccess < b.second->lastAccess; }); + uint64 lastAccess = std::numeric_limits::max(); + if(dropRawItr != m_cacheDecryptedRawBlocks.end()) + lastAccess = dropRawItr->second->lastAccess; + if(dropHashedItr != m_cacheDecryptedHashedBlocks.end()) + lastAccess = std::min(lastAccess, dropHashedItr->second->lastAccess); + if(dropRawItr != m_cacheDecryptedRawBlocks.end() && dropRawItr->second->lastAccess == lastAccess) + { + if (droppedRawBlock) + *droppedRawBlock = dropRawItr->second; + else + delete dropRawItr->second; + m_cacheDecryptedRawBlocks.erase(dropRawItr); + return; + } + else if(dropHashedItr != m_cacheDecryptedHashedBlocks.end() && dropHashedItr->second->lastAccess == lastAccess) + { + if (droppedHashedBlock) + *droppedHashedBlock = dropHashedItr->second; + else + delete dropHashedItr->second; + m_cacheDecryptedHashedBlocks.erase(dropHashedItr); + } +} + +void FSTVolume::DetermineUnhashedBlockIV(uint32 clusterIndex, uint32 blockIndex, uint8 ivOut[16]) +{ + memset(ivOut, 0, sizeof(ivOut)); + if(blockIndex == 0) + { + ivOut[0] = (uint8)(clusterIndex >> 8); + ivOut[1] = (uint8)(clusterIndex >> 0); + } + else + { + // the last 16 encrypted bytes of the previous block are the IV (AES CBC) + // if the previous block is cached we can grab the IV from there. Otherwise we have to read the 16 bytes from the data source + uint32 prevBlockIndex = blockIndex - 1; + uint64 cacheBlockId = ((uint64)clusterIndex << (64 - 16)) | (uint64)prevBlockIndex; + auto itr = m_cacheDecryptedRawBlocks.find(cacheBlockId); + if (itr != m_cacheDecryptedRawBlocks.end()) + { + memcpy(ivOut, itr->second->ivForNextBlock, 16); + } + else + { + cemu_assert(m_sectorSize >= 16); + uint64 clusterOffset = (uint64)m_cluster[clusterIndex].offset * m_sectorSize; + uint8 prevIV[16]; + if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * m_sectorSize - 16, prevIV, 16) != 16) + { + cemuLog_log(LogType::Force, "Failed to read IV for raw FST block"); + m_detectedCorruption = true; + return; + } + memcpy(ivOut, prevIV, 16); + } + } +} + +FSTCachedRawBlock* FSTVolume::GetDecryptedRawBlock(uint32 clusterIndex, uint32 blockIndex) +{ + FSTCluster& cluster = m_cluster[clusterIndex]; + uint64 clusterOffset = (uint64)cluster.offset * m_sectorSize; + // generate id for cache + uint64 cacheBlockId = ((uint64)clusterIndex << (64 - 16)) | (uint64)blockIndex; + // lookup block in cache + FSTCachedRawBlock* block = nullptr; + auto itr = m_cacheDecryptedRawBlocks.find(cacheBlockId); + if (itr != m_cacheDecryptedRawBlocks.end()) + { + block = itr->second; + block->lastAccess = ++m_cacheAccessCounter; + return block; + } + // if cache already full, drop least recently accessed block and recycle FSTCachedRawBlock object if possible + TrimCacheIfRequired(&block, nullptr); + if (!block) + block = new FSTCachedRawBlock(); + block->blockData.rawData.resize(m_sectorSize); + // block not cached, read new + block->lastAccess = ++m_cacheAccessCounter; + if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * m_sectorSize, block->blockData.rawData.data(), m_sectorSize) != m_sectorSize) + { + cemuLog_log(LogType::Force, "Failed to read raw FST block"); + delete block; + m_detectedCorruption = true; + return nullptr; + } + // decrypt hash data + uint8 iv[16]{}; + DetermineUnhashedBlockIV(clusterIndex, blockIndex, iv); + memcpy(block->ivForNextBlock, block->blockData.rawData.data() + m_sectorSize - 16, 16); + AES128_CBC_decrypt(block->blockData.rawData.data(), block->blockData.rawData.data(), m_sectorSize, m_partitionTitlekey.b, iv); + // if this is the next block, then hash it + if(cluster.hasContentHash) + { + if(cluster.singleHashNumBlocksHashed == blockIndex) + { + cemu_assert_debug(!(cluster.contentSize % m_sectorSize)); // size should be multiple of sector size? Regardless, the hashing code below can handle non-aligned sizes + bool isLastBlock = blockIndex == (std::max(cluster.contentSize / m_sectorSize, 1) - 1); + uint32 hashSize = m_sectorSize; + if(isLastBlock) + hashSize = cluster.contentSize - (uint64)blockIndex*m_sectorSize; + EVP_DigestUpdate(cluster.singleHashCtx.get(), block->blockData.rawData.data(), hashSize); + cluster.singleHashNumBlocksHashed++; + if(isLastBlock) + { + uint8 hash[32]; + EVP_DigestFinal_ex(cluster.singleHashCtx.get(), hash, nullptr); + if(memcmp(hash, cluster.contentHash32, cluster.contentHashIsSHA1 ? 20 : 32) != 0) + { + cemuLog_log(LogType::Force, "FST: Raw section hash mismatch"); + delete block; + m_detectedCorruption = true; + return nullptr; + } + } + } + } + // register in cache + m_cacheDecryptedRawBlocks.emplace(cacheBlockId, block); + return block; +} + FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, uint32 blockIndex) { const FSTCluster& cluster = m_cluster[clusterIndex]; @@ -908,22 +1034,17 @@ FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, ui block->lastAccess = ++m_cacheAccessCounter; return block; } - // if cache already full, drop least recently accessed block (but recycle the FSTHashedBlock* object) - if (m_cacheDecryptedHashedBlocks.size() >= 16) - { - auto dropItr = std::min_element(m_cacheDecryptedHashedBlocks.begin(), m_cacheDecryptedHashedBlocks.end(), [](const auto& a, const auto& b) -> bool - { return a.second->lastAccess < b.second->lastAccess; }); - block = dropItr->second; - m_cacheDecryptedHashedBlocks.erase(dropItr); - } - else + // if cache already full, drop least recently accessed block and recycle FSTCachedHashedBlock object if possible + TrimCacheIfRequired(nullptr, &block); + if (!block) block = new FSTCachedHashedBlock(); // block not cached, read new block->lastAccess = ++m_cacheAccessCounter; if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * BLOCK_SIZE, block->blockData.rawData, BLOCK_SIZE) != BLOCK_SIZE) { - cemuLog_log(LogType::Force, "Failed to read FST block"); + cemuLog_log(LogType::Force, "Failed to read hashed FST block"); delete block; + m_detectedCorruption = true; return nullptr; } // decrypt hash data @@ -931,11 +1052,46 @@ FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, ui AES128_CBC_decrypt(block->blockData.getHashData(), block->blockData.getHashData(), BLOCK_HASH_SIZE, m_partitionTitlekey.b, iv); // decrypt file data AES128_CBC_decrypt(block->blockData.getFileData(), block->blockData.getFileData(), BLOCK_FILE_SIZE, m_partitionTitlekey.b, block->blockData.getH0Hash(blockIndex%16)); + // compare with H0 to verify data integrity + NCrypto::CHash160 h0; + SHA1(block->blockData.getFileData(), BLOCK_FILE_SIZE, h0.b); + uint32 h0Index = (blockIndex % 4096); + if (memcmp(h0.b, block->blockData.getH0Hash(h0Index & 0xF), sizeof(h0.b)) != 0) + { + cemuLog_log(LogType::Force, "FST: Hash H0 mismatch in hashed block (section {} index {})", clusterIndex, blockIndex); + delete block; + m_detectedCorruption = true; + return nullptr; + } // register in cache m_cacheDecryptedHashedBlocks.emplace(cacheBlockId, block); return block; } +uint32 FSTVolume::ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut) +{ + uint8* dataOutU8 = (uint8*)dataOut; + if (readOffset >= entry.fileInfo.fileSize) + return 0; + else if ((readOffset + readSize) >= entry.fileInfo.fileSize) + readSize = (entry.fileInfo.fileSize - readOffset); + uint64 absFileOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset; + uint32 remainingReadSize = readSize; + while (remainingReadSize > 0) + { + const FSTCachedRawBlock* rawBlock = this->GetDecryptedRawBlock(clusterIndex, absFileOffset/m_sectorSize); + if (!rawBlock) + break; + uint32 blockOffset = (uint32)(absFileOffset % m_sectorSize); + uint32 bytesToRead = std::min(remainingReadSize, m_sectorSize - blockOffset); + std::memcpy(dataOutU8, rawBlock->blockData.rawData.data() + blockOffset, bytesToRead); + dataOutU8 += bytesToRead; + remainingReadSize -= bytesToRead; + absFileOffset += bytesToRead; + } + return readSize - remainingReadSize; +} + uint32 FSTVolume::ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut) { /* @@ -966,7 +1122,6 @@ uint32 FSTVolume::ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, */ const FSTCluster& cluster = m_cluster[clusterIndex]; - uint64 clusterBaseOffset = (uint64)cluster.offset * m_sectorSize; uint64 fileReadOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset; uint32 blockIndex = (uint32)(fileReadOffset / BLOCK_FILE_SIZE); uint32 bytesRemaining = readSize; @@ -1019,6 +1174,8 @@ bool FSTVolume::Next(FSTDirectoryIterator& directoryIterator, FSTFileHandle& fil FSTVolume::~FSTVolume() { + for (auto& itr : m_cacheDecryptedRawBlocks) + delete itr.second; for (auto& itr : m_cacheDecryptedHashedBlocks) delete itr.second; if (m_sourceIsOwned) @@ -1115,4 +1272,4 @@ bool FSTVerifier::VerifyHashedContentFile(FileStream* fileContent, const NCrypto void FSTVolumeTest() { FSTPathUnitTest(); -} \ No newline at end of file +} diff --git a/src/Cafe/Filesystem/FST/FST.h b/src/Cafe/Filesystem/FST/FST.h index 24fc39ea..601799ce 100644 --- a/src/Cafe/Filesystem/FST/FST.h +++ b/src/Cafe/Filesystem/FST/FST.h @@ -1,5 +1,6 @@ #pragma once #include "Cemu/ncrypto/ncrypto.h" +#include "openssl/evp.h" struct FSTFileHandle { @@ -45,6 +46,7 @@ public: ~FSTVolume(); uint32 GetFileCount() const; + bool HasCorruption() const { return m_detectedCorruption; } bool OpenFile(std::string_view path, FSTFileHandle& fileHandleOut, bool openOnlyFiles = false); @@ -86,15 +88,25 @@ private: enum class ClusterHashMode : uint8 { RAW = 0, // raw data + encryption, no hashing? - RAW2 = 1, // raw data + encryption, with hash stored in tmd? + RAW_STREAM = 1, // raw data + encryption, with hash stored in tmd? HASH_INTERLEAVED = 2, // hashes + raw interleaved in 0x10000 blocks (0x400 bytes of hashes at the beginning, followed by 0xFC00 bytes of data) }; struct FSTCluster { + FSTCluster() : singleHashCtx(nullptr, &EVP_MD_CTX_free) {} + uint32 offset; uint32 size; ClusterHashMode hashMode; + // extra data if TMD is available + bool hasContentHash; + uint8 contentHash32[32]; + bool contentHashIsSHA1; // if true then it's SHA1 (with extra bytes zeroed out), otherwise it's SHA256 + uint64 contentSize; // size of the content (in blocks) + // hash context for single hash mode (content hash must be available) + std::unique_ptr singleHashCtx; // unique_ptr to make this move-only + uint32 singleHashNumBlocksHashed{0}; }; struct FSTEntry @@ -164,17 +176,30 @@ private: bool m_sourceIsOwned{}; uint32 m_sectorSize{}; // for cluster offsets uint32 m_offsetFactor{}; // for file offsets + bool m_hashIsDisabled{}; // disables hash verification (for all clusters of this volume?) std::vector m_cluster; std::vector m_entries; std::vector m_nameStringTable; NCrypto::AesKey m_partitionTitlekey; + bool m_detectedCorruption{false}; - /* Cache for decrypted hashed blocks */ + bool HashIsDisabled() const + { + return m_hashIsDisabled; + } + + /* Cache for decrypted raw and hashed blocks */ + std::unordered_map m_cacheDecryptedRawBlocks; std::unordered_map m_cacheDecryptedHashedBlocks; uint64 m_cacheAccessCounter{}; + void DetermineUnhashedBlockIV(uint32 clusterIndex, uint32 blockIndex, uint8 ivOut[16]); + + struct FSTCachedRawBlock* GetDecryptedRawBlock(uint32 clusterIndex, uint32 blockIndex); struct FSTCachedHashedBlock* GetDecryptedHashedBlock(uint32 clusterIndex, uint32 blockIndex); + void TrimCacheIfRequired(struct FSTCachedRawBlock** droppedRawBlock, struct FSTCachedHashedBlock** droppedHashedBlock); + /* File reading */ uint32 ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut); uint32 ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut); @@ -185,7 +210,10 @@ private: /* +0x00 */ uint32be magic; /* +0x04 */ uint32be offsetFactor; /* +0x08 */ uint32be numCluster; - /* +0x0C */ uint32be ukn0C; + /* +0x0C */ uint8be hashIsDisabled; + /* +0x0D */ uint8be ukn0D; + /* +0x0E */ uint8be ukn0E; + /* +0x0F */ uint8be ukn0F; /* +0x10 */ uint32be ukn10; /* +0x14 */ uint32be ukn14; /* +0x18 */ uint32be ukn18; @@ -262,8 +290,8 @@ private: static_assert(sizeof(FSTHeader_FileEntry) == 0x10); - static FSTVolume* OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode); - static FSTVolume* OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode); + static FSTVolume* OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD); + static FSTVolume* OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD); static bool ProcessFST(FSTHeader_FileEntry* fileTable, uint32 numFileEntries, uint32 numCluster, std::vector& nameStringTable, std::vector& fstEntries); bool MatchFSTEntryName(FSTEntry& entry, std::string_view comparedName) From 66658351c1e274265ddc18643b761f7df1e21e11 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 10 Nov 2024 10:10:46 +0100 Subject: [PATCH 038/137] erreula: Rework implementation and fix bugs - ErrEula doesn't disappear on its own anymore. The expected behavior is for the game to call Disappear once a button has been selected. This fixes issues where the dialog would softlock in some games - Modernized code a bit - Added a subtle fade in/out effect --- src/Cafe/CafeSystem.cpp | 42 +-- src/Cafe/CafeSystem.h | 7 +- src/Cafe/OS/libs/coreinit/coreinit_Time.h | 7 +- src/Cafe/OS/libs/erreula/erreula.cpp | 423 ++++++++++++++-------- src/gui/MainWindow.cpp | 12 +- 5 files changed, 311 insertions(+), 180 deletions(-) diff --git a/src/Cafe/CafeSystem.cpp b/src/Cafe/CafeSystem.cpp index 51de3550..88e0ed3d 100644 --- a/src/Cafe/CafeSystem.cpp +++ b/src/Cafe/CafeSystem.cpp @@ -637,40 +637,40 @@ namespace CafeSystem fsc_unmount("/cemuBossStorage/", FSC_PRIORITY_BASE); } - STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId) + PREPARE_STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId) { cemuLog_log(LogType::Force, "Mounting title {:016x}", (uint64)titleId); sGameInfo_ForegroundTitle = CafeTitleList::GetGameInfo(titleId); if (!sGameInfo_ForegroundTitle.IsValid()) { cemuLog_log(LogType::Force, "Mounting failed: Game meta information is either missing, inaccessible or not valid (missing or invalid .xml files in code and meta folder)"); - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; } // check base TitleInfo& titleBase = sGameInfo_ForegroundTitle.GetBase(); if (!titleBase.IsValid()) - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; if(!titleBase.ParseXmlInfo()) - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; cemuLog_log(LogType::Force, "Base: {}", titleBase.GetPrintPath()); // mount base if (!titleBase.Mount("/vol/content", "content", FSC_PRIORITY_BASE) || !titleBase.Mount(GetInternalVirtualCodeFolder(), "code", FSC_PRIORITY_BASE)) { cemuLog_log(LogType::Force, "Mounting failed"); - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; } // check update TitleInfo& titleUpdate = sGameInfo_ForegroundTitle.GetUpdate(); if (titleUpdate.IsValid()) { if (!titleUpdate.ParseXmlInfo()) - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; cemuLog_log(LogType::Force, "Update: {}", titleUpdate.GetPrintPath()); // mount update if (!titleUpdate.Mount("/vol/content", "content", FSC_PRIORITY_PATCH) || !titleUpdate.Mount(GetInternalVirtualCodeFolder(), "code", FSC_PRIORITY_PATCH)) { cemuLog_log(LogType::Force, "Mounting failed"); - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; } } else @@ -682,20 +682,20 @@ namespace CafeSystem // todo - support for multi-title AOC TitleInfo& titleAOC = aocList[0]; if (!titleAOC.ParseXmlInfo()) - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; cemu_assert_debug(titleAOC.IsValid()); cemuLog_log(LogType::Force, "DLC: {}", titleAOC.GetPrintPath()); // mount AOC if (!titleAOC.Mount(fmt::format("/vol/aoc{:016x}", titleAOC.GetAppTitleId()), "content", FSC_PRIORITY_PATCH)) { cemuLog_log(LogType::Force, "Mounting failed"); - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; } } else cemuLog_log(LogType::Force, "DLC: Not present"); sForegroundTitleId = titleId; - return STATUS_CODE::SUCCESS; + return PREPARE_STATUS_CODE::SUCCESS; } void UnmountForegroundTitle() @@ -723,7 +723,7 @@ namespace CafeSystem } } - STATUS_CODE SetupExecutable() + PREPARE_STATUS_CODE SetupExecutable() { // set rpx path from cos.xml if available _pathToBaseExecutable = _pathToExecutable; @@ -755,7 +755,7 @@ namespace CafeSystem } } LoadMainExecutable(); - return STATUS_CODE::SUCCESS; + return PREPARE_STATUS_CODE::SUCCESS; } void SetupMemorySpace() @@ -769,7 +769,7 @@ namespace CafeSystem memory_unmapForCurrentTitle(); } - STATUS_CODE PrepareForegroundTitle(TitleId titleId) + PREPARE_STATUS_CODE PrepareForegroundTitle(TitleId titleId) { CafeTitleList::WaitForMandatoryScan(); sLaunchModeIsStandalone = false; @@ -780,21 +780,21 @@ namespace CafeSystem // mount mlc storage MountBaseDirectories(); // mount title folders - STATUS_CODE r = LoadAndMountForegroundTitle(titleId); - if (r != STATUS_CODE::SUCCESS) + PREPARE_STATUS_CODE r = LoadAndMountForegroundTitle(titleId); + if (r != PREPARE_STATUS_CODE::SUCCESS) return r; gameProfile_load(); // setup memory space and PPC recompiler SetupMemorySpace(); PPCRecompiler_init(); r = SetupExecutable(); // load RPX - if (r != STATUS_CODE::SUCCESS) + if (r != PREPARE_STATUS_CODE::SUCCESS) return r; InitVirtualMlcStorage(); - return STATUS_CODE::SUCCESS; + return PREPARE_STATUS_CODE::SUCCESS; } - STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path) + PREPARE_STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path) { sLaunchModeIsStandalone = true; cemuLog_log(LogType::Force, "Launching executable in standalone mode due to incorrect layout or missing meta files"); @@ -812,7 +812,7 @@ namespace CafeSystem if (!r) { cemuLog_log(LogType::Force, "Failed to mount {}", _pathToUtf8(contentPath)); - return STATUS_CODE::UNABLE_TO_MOUNT; + return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT; } } } @@ -824,7 +824,7 @@ namespace CafeSystem // since a lot of systems (including save folder location) rely on a TitleId, we derive a placeholder id from the executable hash auto execData = fsc_extractFile(_pathToExecutable.c_str()); if (!execData) - return STATUS_CODE::INVALID_RPX; + return PREPARE_STATUS_CODE::INVALID_RPX; uint32 h = generateHashFromRawRPXData(execData->data(), execData->size()); sForegroundTitleId = 0xFFFFFFFF00000000ULL | (uint64)h; cemuLog_log(LogType::Force, "Generated placeholder TitleId: {:016x}", sForegroundTitleId); @@ -834,7 +834,7 @@ namespace CafeSystem // load executable SetupExecutable(); InitVirtualMlcStorage(); - return STATUS_CODE::SUCCESS; + return PREPARE_STATUS_CODE::SUCCESS; } void _LaunchTitleThread() diff --git a/src/Cafe/CafeSystem.h b/src/Cafe/CafeSystem.h index c4043a59..e9de8d7d 100644 --- a/src/Cafe/CafeSystem.h +++ b/src/Cafe/CafeSystem.h @@ -15,20 +15,19 @@ namespace CafeSystem virtual void CafeRecreateCanvas() = 0; }; - enum class STATUS_CODE + enum class PREPARE_STATUS_CODE { SUCCESS, INVALID_RPX, UNABLE_TO_MOUNT, // failed to mount through TitleInfo (most likely caused by an invalid or outdated path) - //BAD_META_DATA, - the title list only stores titles with valid meta, so this error code is impossible }; void Initialize(); void SetImplementation(SystemImplementation* impl); void Shutdown(); - STATUS_CODE PrepareForegroundTitle(TitleId titleId); - STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path); + PREPARE_STATUS_CODE PrepareForegroundTitle(TitleId titleId); + PREPARE_STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path); void LaunchForegroundTitle(); bool IsTitleRunning(); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Time.h b/src/Cafe/OS/libs/coreinit/coreinit_Time.h index 018e8eb7..3aa92b99 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Time.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Time.h @@ -40,7 +40,12 @@ namespace coreinit inline TimerTicks ConvertNsToTimerTicks(uint64 ns) { - return ((GetTimerClock() / 31250LL) * ((ns)) / 32000LL); + return ((GetTimerClock() / 31250LL) * ((TimerTicks)ns) / 32000LL); + } + + inline TimerTicks ConvertMsToTimerTicks(uint64 ms) + { + return (TimerTicks)ms * GetTimerClock() / 1000LL; } }; diff --git a/src/Cafe/OS/libs/erreula/erreula.cpp b/src/Cafe/OS/libs/erreula/erreula.cpp index a7f2f35c..342e8b64 100644 --- a/src/Cafe/OS/libs/erreula/erreula.cpp +++ b/src/Cafe/OS/libs/erreula/erreula.cpp @@ -9,32 +9,45 @@ #include #include "Cafe/OS/libs/coreinit/coreinit_FS.h" +#include "Cafe/OS/libs/coreinit/coreinit_Time.h" #include "Cafe/OS/libs/vpad/vpad.h" namespace nn { namespace erreula { -#define RESULTTYPE_NONE 0 -#define RESULTTYPE_FINISH 1 -#define RESULTTYPE_NEXT 2 -#define RESULTTYPE_JUMP 3 -#define RESULTTYPE_PASSWORD 4 -#define ERRORTYPE_CODE 0 -#define ERRORTYPE_TEXT 1 -#define ERRORTYPE_TEXT_ONE_BUTTON 2 -#define ERRORTYPE_TEXT_TWO_BUTTON 3 - -#define ERREULA_STATE_HIDDEN 0 -#define ERREULA_STATE_APPEARING 1 -#define ERREULA_STATE_VISIBLE 2 -#define ERREULA_STATE_DISAPPEARING 3 - - struct AppearArg_t + enum class ErrorDialogType : uint32 { - AppearArg_t() = default; - AppearArg_t(const AppearArg_t& o) + Code = 0, + Text = 1, + TextOneButton = 2, + TextTwoButton = 3 + }; + + static const sint32 FADE_TIME = 80; + + enum class ErrEulaState : uint32 + { + Hidden = 0, + Appearing = 1, + Visible = 2, + Disappearing = 3 + }; + + enum class ResultType : uint32 + { + None = 0, + Finish = 1, + Next = 2, + Jump = 3, + Password = 4 + }; + + struct AppearError + { + AppearError() = default; + AppearError(const AppearError& o) { errorType = o.errorType; screenType = o.screenType; @@ -49,7 +62,7 @@ namespace erreula drawCursor = o.drawCursor; } - uint32be errorType; + betype errorType; uint32be screenType; uint32be controllerType; uint32be holdType; @@ -63,7 +76,9 @@ namespace erreula bool drawCursor{}; }; - static_assert(sizeof(AppearArg_t) == 0x2C); // maybe larger + using AppearArg = AppearError; + + static_assert(sizeof(AppearError) == 0x2C); // maybe larger struct HomeNixSignArg_t { @@ -80,6 +95,132 @@ namespace erreula static_assert(sizeof(ControllerInfo_t) == 0x14); // maybe larger + class ErrEulaInstance + { + public: + enum class BUTTON_SELECTION : uint32 + { + NONE = 0xFFFFFFFF, + LEFT = 0, + RIGHT = 1, + }; + + void Init() + { + m_buttonSelection = BUTTON_SELECTION::NONE; + m_resultCode = -1; + m_resultCodeForLeftButton = 0; + m_resultCodeForRightButton = 0; + SetState(ErrEulaState::Hidden); + } + + void DoAppearError(AppearArg* arg) + { + m_buttonSelection = BUTTON_SELECTION::NONE; + m_resultCode = -1; + m_resultCodeForLeftButton = -1; + m_resultCodeForRightButton = -1; + // for standard dialog its 0 and 1? + m_resultCodeForLeftButton = 0; + m_resultCodeForRightButton = 1; + SetState(ErrEulaState::Appearing); + } + + void DoDisappearError() + { + if(m_state != ErrEulaState::Visible) + return; + SetState(ErrEulaState::Disappearing); + } + + void DoCalc() + { + // appearing and disappearing state will automatically advance after some time + if (m_state == ErrEulaState::Appearing || m_state == ErrEulaState::Disappearing) + { + uint32 elapsedTick = coreinit::OSGetTime() - m_lastStateChange; + if (elapsedTick > coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME)) + { + SetState(m_state == ErrEulaState::Appearing ? ErrEulaState::Visible : ErrEulaState::Hidden); + } + } + } + + bool IsDecideSelectButtonError() const + { + return m_buttonSelection != BUTTON_SELECTION::NONE; + } + + bool IsDecideSelectLeftButtonError() const + { + return m_buttonSelection != BUTTON_SELECTION::LEFT; + } + + bool IsDecideSelectRightButtonError() const + { + return m_buttonSelection != BUTTON_SELECTION::RIGHT; + } + + void SetButtonSelection(BUTTON_SELECTION selection) + { + cemu_assert_debug(m_buttonSelection == BUTTON_SELECTION::NONE); + m_buttonSelection = selection; + cemu_assert_debug(selection == BUTTON_SELECTION::LEFT || selection == BUTTON_SELECTION::RIGHT); + m_resultCode = selection == BUTTON_SELECTION::LEFT ? m_resultCodeForLeftButton : m_resultCodeForRightButton; + } + + ErrEulaState GetState() const + { + return m_state; + } + + sint32 GetResultCode() const + { + return m_resultCode; + } + + ResultType GetResultType() const + { + if(m_resultCode == -1) + return ResultType::None; + if(m_resultCode < 10) + return ResultType::Finish; + if(m_resultCode >= 9999) + return ResultType::Next; + if(m_resultCode == 40) + return ResultType::Password; + return ResultType::Jump; + } + + float GetFadeTransparency() const + { + if(m_state == ErrEulaState::Appearing || m_state == ErrEulaState::Disappearing) + { + uint32 elapsedTick = coreinit::OSGetTime() - m_lastStateChange; + if(m_state == ErrEulaState::Appearing) + return std::min(1.0f, (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME)); + else + return std::max(0.0f, 1.0f - (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME)); + } + return 1.0f; + } + + private: + void SetState(ErrEulaState state) + { + m_state = state; + m_lastStateChange = coreinit::OSGetTime(); + } + + ErrEulaState m_state; + uint32 m_lastStateChange; + + /* +0x30 */ betype m_resultCode; + /* +0x239C */ betype m_buttonSelection; + /* +0x23A0 */ betype m_resultCodeForLeftButton; + /* +0x23A4 */ betype m_resultCodeForRightButton; + }; + struct ErrEula_t { SysAllocator mutex; @@ -87,17 +228,11 @@ namespace erreula uint32 langType; MEMPTR fsClient; - AppearArg_t currentDialog; - uint32 state; - bool buttonPressed; - bool rightButtonPressed; + std::unique_ptr errEulaInstance; + AppearError currentDialog; bool homeNixSignVisible; - - std::chrono::steady_clock::time_point stateTimer{}; } g_errEula = {}; - - std::wstring GetText(uint16be* text) { @@ -113,22 +248,61 @@ namespace erreula } - void export_ErrEulaCreate(PPCInterpreter_t* hCPU) + void ErrEulaCreate(void* workmem, uint32 regionType, uint32 langType, coreinit::FSClient_t* fsClient) { - ppcDefineParamMEMPTR(thisptr, uint8, 0); - ppcDefineParamU32(regionType, 1); - ppcDefineParamU32(langType, 2); - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 3); - coreinit::OSLockMutex(&g_errEula.mutex); g_errEula.regionType = regionType; g_errEula.langType = langType; g_errEula.fsClient = fsClient; + cemu_assert_debug(!g_errEula.errEulaInstance); + g_errEula.errEulaInstance = std::make_unique(); + g_errEula.errEulaInstance->Init(); coreinit::OSUnlockMutex(&g_errEula.mutex); + } - osLib_returnFromFunction(hCPU, 0); + void ErrEulaDestroy() + { + g_errEula.errEulaInstance.reset(); + } + + // check if any dialog button was selected + bool IsDecideSelectButtonError() + { + if(!g_errEula.errEulaInstance) + return false; + return g_errEula.errEulaInstance->IsDecideSelectButtonError(); + } + + // check if left dialog button was selected + bool IsDecideSelectLeftButtonError() + { + if(!g_errEula.errEulaInstance) + return false; + return g_errEula.errEulaInstance->IsDecideSelectLeftButtonError(); + } + + // check if right dialog button was selected + bool IsDecideSelectRightButtonError() + { + if(!g_errEula.errEulaInstance) + return false; + return g_errEula.errEulaInstance->IsDecideSelectRightButtonError(); + } + + sint32 GetResultCode() + { + if(!g_errEula.errEulaInstance) + return -1; + return g_errEula.errEulaInstance->GetResultCode(); + } + + ResultType GetResultType() + { + if(!g_errEula.errEulaInstance) + return ResultType::None; + return g_errEula.errEulaInstance->GetResultType(); } void export_AppearHomeNixSign(PPCInterpreter_t* hCPU) @@ -137,28 +311,24 @@ namespace erreula osLib_returnFromFunction(hCPU, 0); } - void export_AppearError(PPCInterpreter_t* hCPU) + void ErrEulaAppearError(AppearArg* arg) { - ppcDefineParamMEMPTR(arg, AppearArg_t, 0); - - g_errEula.currentDialog = *arg.GetPtr(); - g_errEula.state = ERREULA_STATE_APPEARING; - g_errEula.buttonPressed = false; - g_errEula.rightButtonPressed = false; - - g_errEula.stateTimer = tick_cached(); - - osLib_returnFromFunction(hCPU, 0); + g_errEula.currentDialog = *arg; + if(g_errEula.errEulaInstance) + g_errEula.errEulaInstance->DoAppearError(arg); } - void export_GetStateErrorViewer(PPCInterpreter_t* hCPU) + void ErrEulaDisappearError() { - osLib_returnFromFunction(hCPU, g_errEula.state); + if(g_errEula.errEulaInstance) + g_errEula.errEulaInstance->DoDisappearError(); } - void export_DisappearError(PPCInterpreter_t* hCPU) + + ErrEulaState ErrEulaGetStateErrorViewer() { - g_errEula.state = ERREULA_STATE_HIDDEN; - osLib_returnFromFunction(hCPU, 0); + if(!g_errEula.errEulaInstance) + return ErrEulaState::Hidden; + return g_errEula.errEulaInstance->GetState(); } void export_ChangeLang(PPCInterpreter_t* hCPU) @@ -168,27 +338,6 @@ namespace erreula osLib_returnFromFunction(hCPU, 0); } - void export_IsDecideSelectButtonError(PPCInterpreter_t* hCPU) - { - if (g_errEula.buttonPressed) - cemuLog_logDebug(LogType::Force, "IsDecideSelectButtonError: TRUE"); - osLib_returnFromFunction(hCPU, g_errEula.buttonPressed); - } - - void export_IsDecideSelectLeftButtonError(PPCInterpreter_t* hCPU) - { - if (g_errEula.buttonPressed) - cemuLog_logDebug(LogType::Force, "IsDecideSelectLeftButtonError: TRUE"); - osLib_returnFromFunction(hCPU, g_errEula.buttonPressed); - } - - void export_IsDecideSelectRightButtonError(PPCInterpreter_t* hCPU) - { - if (g_errEula.rightButtonPressed) - cemuLog_logDebug(LogType::Force, "IsDecideSelectRightButtonError: TRUE"); - osLib_returnFromFunction(hCPU, g_errEula.rightButtonPressed); - } - void export_IsAppearHomeNixSign(PPCInterpreter_t* hCPU) { osLib_returnFromFunction(hCPU, g_errEula.homeNixSignVisible); @@ -200,61 +349,19 @@ namespace erreula osLib_returnFromFunction(hCPU, 0); } - void export_GetResultType(PPCInterpreter_t* hCPU) + void ErrEulaCalc(ControllerInfo_t* controllerInfo) { - uint32 result = RESULTTYPE_NONE; - if (g_errEula.buttonPressed || g_errEula.rightButtonPressed) - { - cemuLog_logDebug(LogType::Force, "GetResultType: FINISH"); - result = RESULTTYPE_FINISH; - } - - osLib_returnFromFunction(hCPU, result); - } - - void export_Calc(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(controllerInfo, ControllerInfo_t, 0); - // TODO: check controller buttons bla to accept dialog? - osLib_returnFromFunction(hCPU, 0); + if(g_errEula.errEulaInstance) + g_errEula.errEulaInstance->DoCalc(); } void render(bool mainWindow) { - if(g_errEula.state == ERREULA_STATE_HIDDEN) + if(!g_errEula.errEulaInstance) return; - - if(g_errEula.state == ERREULA_STATE_APPEARING) - { - if(std::chrono::duration_cast(tick_cached() - g_errEula.stateTimer).count() <= 1000) - { - return; - } - - g_errEula.state = ERREULA_STATE_VISIBLE; - g_errEula.stateTimer = tick_cached(); - } - /*else if(g_errEula.state == STATE_VISIBLE) - { - if (std::chrono::duration_cast(tick_cached() - g_errEula.stateTimer).count() >= 1000) - { - g_errEula.state = STATE_DISAPPEARING; - g_errEula.stateTimer = tick_cached(); - return; - } - }*/ - else if(g_errEula.state == ERREULA_STATE_DISAPPEARING) - { - if (std::chrono::duration_cast(tick_cached() - g_errEula.stateTimer).count() >= 2000) - { - g_errEula.state = ERREULA_STATE_HIDDEN; - g_errEula.stateTimer = tick_cached(); - } - + if(g_errEula.errEulaInstance->GetState() != ErrEulaState::Visible && g_errEula.errEulaInstance->GetState() != ErrEulaState::Appearing && g_errEula.errEulaInstance->GetState() != ErrEulaState::Disappearing) return; - } - - const AppearArg_t& appearArg = g_errEula.currentDialog; + const AppearError& appearArg = g_errEula.currentDialog; std::string text; const uint32 errorCode = (uint32)appearArg.errorCode; if (errorCode != 0) @@ -276,17 +383,28 @@ namespace erreula ImGui::SetNextWindowPos(position, ImGuiCond_Always, pivot); ImGui::SetNextWindowBgAlpha(0.9f); ImGui::PushFont(font); - + std::string title; if (appearArg.title) title = boost::nowide::narrow(GetText(appearArg.title.GetPtr())); - if(title.empty()) // ImGui doesn't allow empty titles, so set one if appearArg.title is not set or empty + if (title.empty()) // ImGui doesn't allow empty titles, so set one if appearArg.title is not set or empty title = "ErrEula"; + + float fadeTransparency = 1.0f; + if (g_errEula.errEulaInstance->GetState() == ErrEulaState::Appearing || g_errEula.errEulaInstance->GetState() == ErrEulaState::Disappearing) + { + fadeTransparency = g_errEula.errEulaInstance->GetFadeTransparency(); + } + + float originalAlpha = ImGui::GetStyle().Alpha; + ImGui::GetStyle().Alpha = fadeTransparency; + ImGui::SetNextWindowBgAlpha(0.9f * fadeTransparency); if (ImGui::Begin(title.c_str(), nullptr, kPopupFlags)) { const float startx = ImGui::GetWindowSize().x / 2.0f; + bool hasLeftButtonPressed = false, hasRightButtonPressed = false; - switch ((uint32)appearArg.errorType) + switch (appearArg.errorType) { default: { @@ -294,11 +412,10 @@ namespace erreula ImGui::TextUnformatted(text.c_str(), text.c_str() + text.size()); ImGui::Spacing(); ImGui::SetCursorPosX(startx - 50); - g_errEula.buttonPressed |= ImGui::Button("OK", {100, 0}); - + hasLeftButtonPressed = ImGui::Button("OK", {100, 0}); break; } - case ERRORTYPE_TEXT: + case ErrorDialogType::Text: { std::string txtTmp = "Unknown Error"; if (appearArg.text) @@ -309,10 +426,10 @@ namespace erreula ImGui::Spacing(); ImGui::SetCursorPosX(startx - 50); - g_errEula.buttonPressed |= ImGui::Button("OK", { 100, 0 }); + hasLeftButtonPressed = ImGui::Button("OK", { 100, 0 }); break; } - case ERRORTYPE_TEXT_ONE_BUTTON: + case ErrorDialogType::TextOneButton: { std::string txtTmp = "Unknown Error"; if (appearArg.text) @@ -328,10 +445,10 @@ namespace erreula float width = std::max(100.0f, ImGui::CalcTextSize(button1.c_str()).x + 10.0f); ImGui::SetCursorPosX(startx - (width / 2.0f)); - g_errEula.buttonPressed |= ImGui::Button(button1.c_str(), { width, 0 }); + hasLeftButtonPressed = ImGui::Button(button1.c_str(), { width, 0 }); break; } - case ERRORTYPE_TEXT_TWO_BUTTON: + case ErrorDialogType::TextTwoButton: { std::string txtTmp = "Unknown Error"; if (appearArg.text) @@ -352,42 +469,52 @@ namespace erreula float width2 = std::max(100.0f, ImGui::CalcTextSize(button2.c_str()).x + 10.0f); ImGui::SetCursorPosX(startx - (width1 / 2.0f) - (width2 / 2.0f) - 10); - g_errEula.buttonPressed |= ImGui::Button(button1.c_str(), { width1, 0 }); + hasLeftButtonPressed = ImGui::Button(button1.c_str(), { width1, 0 }); ImGui::SameLine(); - g_errEula.rightButtonPressed |= ImGui::Button(button2.c_str(), { width2, 0 }); + hasRightButtonPressed = ImGui::Button(button2.c_str(), { width2, 0 }); break; } } + if (!g_errEula.errEulaInstance->IsDecideSelectButtonError()) + { + if (hasLeftButtonPressed) + g_errEula.errEulaInstance->SetButtonSelection(ErrEulaInstance::BUTTON_SELECTION::LEFT); + if (hasRightButtonPressed) + g_errEula.errEulaInstance->SetButtonSelection(ErrEulaInstance::BUTTON_SELECTION::RIGHT); + } } ImGui::End(); ImGui::PopFont(); - - if(g_errEula.buttonPressed || g_errEula.rightButtonPressed) - { - g_errEula.state = ERREULA_STATE_DISAPPEARING; - g_errEula.stateTimer = tick_cached(); - } + ImGui::GetStyle().Alpha = originalAlpha; } void load() { + g_errEula.errEulaInstance.reset(); + OSInitMutexEx(&g_errEula.mutex, nullptr); - //osLib_addFunction("erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10", export_ErrEulaCreate); // copy ctor? - osLib_addFunction("erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10RegionTypeQ3_2nn7erreula8LangTypeP8FSClient", export_ErrEulaCreate); + cafeExportRegisterFunc(ErrEulaCreate, "erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10RegionTypeQ3_2nn7erreula8LangTypeP8FSClient", LogType::Placeholder); + cafeExportRegisterFunc(ErrEulaDestroy, "erreula", "ErrEulaDestroy__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(IsDecideSelectButtonError, "erreula", "ErrEulaIsDecideSelectButtonError__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(IsDecideSelectLeftButtonError, "erreula", "ErrEulaIsDecideSelectLeftButtonError__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(IsDecideSelectRightButtonError, "erreula", "ErrEulaIsDecideSelectRightButtonError__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(GetResultCode, "erreula", "ErrEulaGetResultCode__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(GetResultType, "erreula", "ErrEulaGetResultType__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(ErrEulaAppearError, "erreula", "ErrEulaAppearError__3RplFRCQ3_2nn7erreula9AppearArg", LogType::Placeholder); + cafeExportRegisterFunc(ErrEulaDisappearError, "erreula", "ErrEulaDisappearError__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(ErrEulaGetStateErrorViewer, "erreula", "ErrEulaGetStateErrorViewer__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(ErrEulaCalc, "erreula", "ErrEulaCalc__3RplFRCQ3_2nn7erreula14ControllerInfo", LogType::Placeholder); + osLib_addFunction("erreula", "ErrEulaAppearHomeNixSign__3RplFRCQ3_2nn7erreula14HomeNixSignArg", export_AppearHomeNixSign); - osLib_addFunction("erreula", "ErrEulaAppearError__3RplFRCQ3_2nn7erreula9AppearArg", export_AppearError); - osLib_addFunction("erreula", "ErrEulaGetStateErrorViewer__3RplFv", export_GetStateErrorViewer); osLib_addFunction("erreula", "ErrEulaChangeLang__3RplFQ3_2nn7erreula8LangType", export_ChangeLang); - osLib_addFunction("erreula", "ErrEulaIsDecideSelectButtonError__3RplFv", export_IsDecideSelectButtonError); - osLib_addFunction("erreula", "ErrEulaCalc__3RplFRCQ3_2nn7erreula14ControllerInfo", export_Calc); - osLib_addFunction("erreula", "ErrEulaIsDecideSelectLeftButtonError__3RplFv", export_IsDecideSelectLeftButtonError); - osLib_addFunction("erreula", "ErrEulaIsDecideSelectRightButtonError__3RplFv", export_IsDecideSelectRightButtonError); osLib_addFunction("erreula", "ErrEulaIsAppearHomeNixSign__3RplFv", export_IsAppearHomeNixSign); osLib_addFunction("erreula", "ErrEulaDisappearHomeNixSign__3RplFv", export_DisappearHomeNixSign); - osLib_addFunction("erreula", "ErrEulaGetResultType__3RplFv", export_GetResultType); - osLib_addFunction("erreula", "ErrEulaDisappearError__3RplFv", export_DisappearError); } } } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c83ab16b..69ff4e99 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -483,20 +483,20 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } - CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId); - if (r == CafeSystem::STATUS_CODE::INVALID_RPX) + CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId); + if (r == CafeSystem::PREPARE_STATUS_CODE::INVALID_RPX) { cemu_assert_debug(false); return false; } - else if (r == CafeSystem::STATUS_CODE::UNABLE_TO_MOUNT) + else if (r == CafeSystem::PREPARE_STATUS_CODE::UNABLE_TO_MOUNT) { wxString t = _("Unable to mount title.\nMake sure the configured game paths are still valid and refresh the game list.\n\nFile which failed to load:\n"); t.append(_pathToUtf8(launchPath)); wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } - else if (r != CafeSystem::STATUS_CODE::SUCCESS) + else if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) { wxString t = _("Failed to launch game."); t.append(_pathToUtf8(launchPath)); @@ -511,8 +511,8 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath); if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF) { - CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); - if (r != CafeSystem::STATUS_CODE::SUCCESS) + CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); + if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) { cemu_assert_debug(false); // todo wxString t = _("Failed to launch executable. Path: "); From 719c631f13c451e045903e79768e98e264a0e9b2 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 13 Nov 2024 06:28:13 +0100 Subject: [PATCH 039/137] config: Fix receive_untested_updates using the wrong default --- src/config/CemuConfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index f5ee7ab4..26f420a5 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -38,7 +38,7 @@ void CemuConfig::Load(XMLConfigParser& parser) fullscreen_menubar = parser.get("fullscreen_menubar", false); feral_gamemode = parser.get("feral_gamemode", false); check_update = parser.get("check_update", check_update); - receive_untested_updates = parser.get("receive_untested_updates", check_update); + receive_untested_updates = parser.get("receive_untested_updates", receive_untested_updates); save_screenshot = parser.get("save_screenshot", save_screenshot); did_show_vulkan_warning = parser.get("vk_warning", did_show_vulkan_warning); did_show_graphic_pack_download = parser.get("gp_download", did_show_graphic_pack_download); From 6f9f3d52ea12d3951b2d9d71806f18fea46140e3 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 13 Nov 2024 06:38:13 +0100 Subject: [PATCH 040/137] CI: Remove outdated workflow --- ...imental_release.yml => deploy_release.yml} | 4 +- .github/workflows/deploy_stable_release.yml | 85 ------------------- 2 files changed, 2 insertions(+), 87 deletions(-) rename .github/workflows/{deploy_experimental_release.yml => deploy_release.yml} (98%) delete mode 100644 .github/workflows/deploy_stable_release.yml diff --git a/.github/workflows/deploy_experimental_release.yml b/.github/workflows/deploy_release.yml similarity index 98% rename from .github/workflows/deploy_experimental_release.yml rename to .github/workflows/deploy_release.yml index 97e0c69e..2b9ee491 100644 --- a/.github/workflows/deploy_experimental_release.yml +++ b/.github/workflows/deploy_release.yml @@ -1,4 +1,4 @@ -name: Deploy experimental release +name: Deploy release on: workflow_dispatch: inputs: @@ -54,7 +54,7 @@ jobs: next_version_major: ${{ needs.calculate-version.outputs.next_version_major }} next_version_minor: ${{ needs.calculate-version.outputs.next_version_minor }} deploy: - name: Deploy experimental release + name: Deploy release runs-on: ubuntu-22.04 needs: [call-release-build, calculate-version] steps: diff --git a/.github/workflows/deploy_stable_release.yml b/.github/workflows/deploy_stable_release.yml deleted file mode 100644 index fd339e7d..00000000 --- a/.github/workflows/deploy_stable_release.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Create new release -on: - workflow_dispatch: - inputs: - PlaceholderInput: - description: PlaceholderInput - required: false -jobs: - call-release-build: - uses: ./.github/workflows/build.yml - with: - deploymode: release - deploy: - name: Deploy release - runs-on: ubuntu-20.04 - needs: call-release-build - steps: - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v4 - with: - name: cemu-bin-linux-x64 - path: cemu-bin-linux-x64 - - - uses: actions/download-artifact@v4 - with: - name: cemu-appimage-x64 - path: cemu-appimage-x64 - - - uses: actions/download-artifact@v4 - with: - name: cemu-bin-windows-x64 - path: cemu-bin-windows-x64 - - - uses: actions/download-artifact@v4 - with: - name: cemu-bin-macos-x64 - path: cemu-bin-macos-x64 - - - name: Initialize - run: | - mkdir upload - sudo apt update -qq - sudo apt install -y zip - - - name: Get Cemu release version - run: | - gcc -o getversion .github/getversion.cpp - echo "Cemu CI version: $(./getversion)" - echo "CEMU_FOLDER_NAME=Cemu_$(./getversion)" >> $GITHUB_ENV - echo "CEMU_VERSION=$(./getversion)" >> $GITHUB_ENV - - - name: Create release from windows-bin - run: | - ls ./ - ls ./bin/ - cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }} - mv cemu-bin-windows-x64/Cemu.exe ./${{ env.CEMU_FOLDER_NAME }}/Cemu.exe - zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-windows-x64.zip ${{ env.CEMU_FOLDER_NAME }} - rm -r ./${{ env.CEMU_FOLDER_NAME }} - - - name: Create appimage - run: | - VERSION=${{ env.CEMU_VERSION }} - echo "Cemu Version is $VERSION" - ls cemu-appimage-x64 - mv cemu-appimage-x64/Cemu-*-x86_64.AppImage upload/Cemu-$VERSION-x86_64.AppImage - - - name: Create release from ubuntu-bin - run: | - ls ./ - ls ./bin/ - cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }} - mv cemu-bin-linux-x64/Cemu ./${{ env.CEMU_FOLDER_NAME }}/Cemu - zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-ubuntu-20.04-x64.zip ${{ env.CEMU_FOLDER_NAME }} - rm -r ./${{ env.CEMU_FOLDER_NAME }} - - - name: Create release from macos-bin - run: cp cemu-bin-macos-x64/Cemu.dmg upload/cemu-${{ env.CEMU_VERSION }}-macos-12-x64.dmg - - - name: Create release - run: | - wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.15.0/ghr_v0.15.0_linux_amd64.tar.gz - tar xvzf ghr.tar.gz; rm ghr.tar.gz - ghr_v0.15.0_linux_amd64/ghr -t ${{ secrets.GITHUB_TOKEN }} -n "Cemu ${{ env.CEMU_VERSION }}" -b "Changelog:" v${{ env.CEMU_VERSION }} ./upload From 269d5b9aabc2a346f441cae5e662fb32fbb7da41 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Sat, 16 Nov 2024 10:02:43 +0100 Subject: [PATCH 041/137] Vulkan: Make scaling shaders compatible + fixes (#1392) --- src/Cafe/GraphicPack/GraphicPack2.cpp | 2 +- src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp | 9 +- .../Latte/Renderer/OpenGL/OpenGLRenderer.cpp | 11 +- .../HW/Latte/Renderer/RendererOuputShader.cpp | 211 +++++++----------- .../HW/Latte/Renderer/RendererOuputShader.h | 14 +- .../Renderer/Vulkan/LatteTextureViewVk.cpp | 10 + .../Latte/Renderer/Vulkan/VulkanRenderer.cpp | 27 +++ 7 files changed, 135 insertions(+), 149 deletions(-) diff --git a/src/Cafe/GraphicPack/GraphicPack2.cpp b/src/Cafe/GraphicPack/GraphicPack2.cpp index c54c31cb..4b6cb095 100644 --- a/src/Cafe/GraphicPack/GraphicPack2.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2.cpp @@ -960,7 +960,7 @@ bool GraphicPack2::Activate() auto option_upscale = rules.FindOption("upscaleMagFilter"); if(option_upscale && boost::iequals(*option_upscale, "NearestNeighbor")) m_output_settings.upscale_filter = LatteTextureView::MagFilter::kNearestNeighbor; - auto option_downscale = rules.FindOption("NearestNeighbor"); + auto option_downscale = rules.FindOption("downscaleMinFilter"); if (option_downscale && boost::iequals(*option_downscale, "NearestNeighbor")) m_output_settings.downscale_filter = LatteTextureView::MagFilter::kNearestNeighbor; } diff --git a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp index ca6a2a4d..3bb6c7e3 100644 --- a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp +++ b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp @@ -933,13 +933,6 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa if (shader == nullptr) { sint32 scaling_filter = downscaling ? GetConfig().downscale_filter : GetConfig().upscale_filter; - - if (g_renderer->GetType() == RendererAPI::Vulkan) - { - // force linear or nearest neighbor filter - if(scaling_filter != kLinearFilter && scaling_filter != kNearestNeighborFilter) - scaling_filter = kLinearFilter; - } if (scaling_filter == kLinearFilter) { @@ -957,7 +950,7 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa else shader = RendererOutputShader::s_bicubic_shader; - filter = LatteTextureView::MagFilter::kNearestNeighbor; + filter = LatteTextureView::MagFilter::kLinear; } else if (scaling_filter == kBicubicHermiteFilter) { diff --git a/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp b/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp index cf134a5d..bbf988bc 100644 --- a/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp @@ -570,13 +570,10 @@ void OpenGLRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu g_renderer->ClearColorbuffer(padView); } - sint32 effectiveWidth, effectiveHeight; - texView->baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0); - shader_unbind(RendererShader::ShaderType::kGeometry); shader_bind(shader->GetVertexShader()); shader_bind(shader->GetFragmentShader()); - shader->SetUniformParameters(*texView, { effectiveWidth, effectiveHeight }, { imageWidth, imageHeight }); + shader->SetUniformParameters(*texView, {imageWidth, imageHeight}); // set viewport glViewportIndexedf(0, imageX, imageY, imageWidth, imageHeight); @@ -584,6 +581,12 @@ void OpenGLRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu LatteTextureViewGL* texViewGL = (LatteTextureViewGL*)texView; texture_bindAndActivate(texView, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + texViewGL->samplerState.clampS = texViewGL->samplerState.clampT = 0xFF; + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, useLinearTexFilter ? GL_LINEAR : GL_NEAREST); + texViewGL->samplerState.filterMin = 0xFFFFFFFF; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, useLinearTexFilter ? GL_LINEAR : GL_NEAREST); texViewGL->samplerState.filterMag = 0xFFFFFFFF; diff --git a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp index cdbeb3f3..409dc24f 100644 --- a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp +++ b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp @@ -2,18 +2,7 @@ #include "Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.h" const std::string RendererOutputShader::s_copy_shader_source = -R"(#version 420 - -#ifdef VULKAN -layout(location = 0) in vec2 passUV; -layout(binding = 0) uniform sampler2D textureSrc; -layout(location = 0) out vec4 colorOut0; -#else -in vec2 passUV; -layout(binding=0) uniform sampler2D textureSrc; -layout(location = 0) out vec4 colorOut0; -#endif - +R"( void main() { colorOut0 = vec4(texture(textureSrc, passUV).rgb,1.0); @@ -22,20 +11,6 @@ void main() const std::string RendererOutputShader::s_bicubic_shader_source = R"( -#version 420 - -#ifdef VULKAN -layout(location = 0) in vec2 passUV; -layout(binding = 0) uniform sampler2D textureSrc; -layout(binding = 1) uniform vec2 textureSrcResolution; -layout(location = 0) out vec4 colorOut0; -#else -in vec2 passUV; -layout(binding=0) uniform sampler2D textureSrc; -uniform vec2 textureSrcResolution; -layout(location = 0) out vec4 colorOut0; -#endif - vec4 cubic(float x) { float x2 = x * x; @@ -48,24 +23,23 @@ vec4 cubic(float x) return w / 6.0; } -vec4 bcFilter(vec2 texcoord, vec2 texscale) +vec4 bcFilter(vec2 uv, vec4 texelSize) { - float fx = fract(texcoord.x); - float fy = fract(texcoord.y); - texcoord.x -= fx; - texcoord.y -= fy; + vec2 pixel = uv*texelSize.zw - 0.5; + vec2 pixelFrac = fract(pixel); + vec2 pixelInt = pixel - pixelFrac; - vec4 xcubic = cubic(fx); - vec4 ycubic = cubic(fy); + vec4 xcubic = cubic(pixelFrac.x); + vec4 ycubic = cubic(pixelFrac.y); - vec4 c = vec4(texcoord.x - 0.5, texcoord.x + 1.5, texcoord.y - 0.5, texcoord.y + 1.5); + vec4 c = vec4(pixelInt.x - 0.5, pixelInt.x + 1.5, pixelInt.y - 0.5, pixelInt.y + 1.5); vec4 s = vec4(xcubic.x + xcubic.y, xcubic.z + xcubic.w, ycubic.x + ycubic.y, ycubic.z + ycubic.w); vec4 offset = c + vec4(xcubic.y, xcubic.w, ycubic.y, ycubic.w) / s; - vec4 sample0 = texture(textureSrc, vec2(offset.x, offset.z) * texscale); - vec4 sample1 = texture(textureSrc, vec2(offset.y, offset.z) * texscale); - vec4 sample2 = texture(textureSrc, vec2(offset.x, offset.w) * texscale); - vec4 sample3 = texture(textureSrc, vec2(offset.y, offset.w) * texscale); + vec4 sample0 = texture(textureSrc, vec2(offset.x, offset.z) * texelSize.xy); + vec4 sample1 = texture(textureSrc, vec2(offset.y, offset.z) * texelSize.xy); + vec4 sample2 = texture(textureSrc, vec2(offset.x, offset.w) * texelSize.xy); + vec4 sample3 = texture(textureSrc, vec2(offset.y, offset.w) * texelSize.xy); float sx = s.x / (s.x + s.y); float sy = s.z / (s.z + s.w); @@ -76,20 +50,13 @@ vec4 bcFilter(vec2 texcoord, vec2 texscale) } void main(){ - colorOut0 = vec4(bcFilter(passUV*textureSrcResolution, vec2(1.0,1.0)/textureSrcResolution).rgb,1.0); + vec4 texelSize = vec4( 1.0 / textureSrcResolution.xy, textureSrcResolution.xy); + colorOut0 = vec4(bcFilter(passUV, texelSize).rgb,1.0); } )"; const std::string RendererOutputShader::s_hermite_shader_source = -R"(#version 420 - -in vec4 gl_FragCoord; -in vec2 passUV; -layout(binding=0) uniform sampler2D textureSrc; -uniform vec2 textureSrcResolution; -uniform vec2 outputResolution; -layout(location = 0) out vec4 colorOut0; - +R"( // https://www.shadertoy.com/view/MllSzX vec3 CubicHermite (vec3 A, vec3 B, vec3 C, vec3 D, float t) @@ -111,7 +78,7 @@ vec3 BicubicHermiteTexture(vec2 uv, vec4 texelSize) vec2 frac = fract(pixel); pixel = floor(pixel) / texelSize.zw - vec2(texelSize.xy/2.0); - vec4 doubleSize = texelSize*texelSize; + vec4 doubleSize = texelSize*2.0; vec3 C00 = texture(textureSrc, pixel + vec2(-texelSize.x ,-texelSize.y)).rgb; vec3 C10 = texture(textureSrc, pixel + vec2( 0.0 ,-texelSize.y)).rgb; @@ -142,15 +109,17 @@ vec3 BicubicHermiteTexture(vec2 uv, vec4 texelSize) } void main(){ - vec4 texelSize = vec4( 1.0 / outputResolution.xy, outputResolution.xy); + vec4 texelSize = vec4( 1.0 / textureSrcResolution.xy, textureSrcResolution.xy); colorOut0 = vec4(BicubicHermiteTexture(passUV, texelSize), 1.0); } )"; RendererOutputShader::RendererOutputShader(const std::string& vertex_source, const std::string& fragment_source) { + auto finalFragmentSrc = PrependFragmentPreamble(fragment_source); + m_vertex_shader = g_renderer->shader_create(RendererShader::ShaderType::kVertex, 0, 0, vertex_source, false, false); - m_fragment_shader = g_renderer->shader_create(RendererShader::ShaderType::kFragment, 0, 0, fragment_source, false, false); + m_fragment_shader = g_renderer->shader_create(RendererShader::ShaderType::kFragment, 0, 0, finalFragmentSrc, false, false); m_vertex_shader->PreponeCompilation(true); m_fragment_shader->PreponeCompilation(true); @@ -163,74 +132,45 @@ RendererOutputShader::RendererOutputShader(const std::string& vertex_source, con if (g_renderer->GetType() == RendererAPI::OpenGL) { - m_attributes[0].m_loc_texture_src_resolution = m_vertex_shader->GetUniformLocation("textureSrcResolution"); - m_attributes[0].m_loc_input_resolution = m_vertex_shader->GetUniformLocation("inputResolution"); - m_attributes[0].m_loc_output_resolution = m_vertex_shader->GetUniformLocation("outputResolution"); + m_uniformLocations[0].m_loc_textureSrcResolution = m_vertex_shader->GetUniformLocation("textureSrcResolution"); + m_uniformLocations[0].m_loc_nativeResolution = m_vertex_shader->GetUniformLocation("nativeResolution"); + m_uniformLocations[0].m_loc_outputResolution = m_vertex_shader->GetUniformLocation("outputResolution"); - m_attributes[1].m_loc_texture_src_resolution = m_fragment_shader->GetUniformLocation("textureSrcResolution"); - m_attributes[1].m_loc_input_resolution = m_fragment_shader->GetUniformLocation("inputResolution"); - m_attributes[1].m_loc_output_resolution = m_fragment_shader->GetUniformLocation("outputResolution"); + m_uniformLocations[1].m_loc_textureSrcResolution = m_fragment_shader->GetUniformLocation("textureSrcResolution"); + m_uniformLocations[1].m_loc_nativeResolution = m_fragment_shader->GetUniformLocation("nativeResolution"); + m_uniformLocations[1].m_loc_outputResolution = m_fragment_shader->GetUniformLocation("outputResolution"); } - else - { - cemuLog_logDebug(LogType::Force, "RendererOutputShader() - todo for Vulkan"); - m_attributes[0].m_loc_texture_src_resolution = -1; - m_attributes[0].m_loc_input_resolution = -1; - m_attributes[0].m_loc_output_resolution = -1; - - m_attributes[1].m_loc_texture_src_resolution = -1; - m_attributes[1].m_loc_input_resolution = -1; - m_attributes[1].m_loc_output_resolution = -1; - } - } -void RendererOutputShader::SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& input_res, const Vector2i& output_res) const +void RendererOutputShader::SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& output_res) const { - float res[2]; - // vertex shader - if (m_attributes[0].m_loc_texture_src_resolution != -1) - { - res[0] = (float)texture_view.baseTexture->width; - res[1] = (float)texture_view.baseTexture->height; - m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_texture_src_resolution, res, 1); - } + sint32 effectiveWidth, effectiveHeight; + texture_view.baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0); + auto setUniforms = [&](RendererShader* shader, const UniformLocations& locations){ + float res[2]; + if (locations.m_loc_textureSrcResolution != -1) + { + res[0] = (float)effectiveWidth; + res[1] = (float)effectiveHeight; + shader->SetUniform2fv(locations.m_loc_textureSrcResolution, res, 1); + } - if (m_attributes[0].m_loc_input_resolution != -1) - { - res[0] = (float)input_res.x; - res[1] = (float)input_res.y; - m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_input_resolution, res, 1); - } + if (locations.m_loc_nativeResolution != -1) + { + res[0] = (float)texture_view.baseTexture->width; + res[1] = (float)texture_view.baseTexture->height; + shader->SetUniform2fv(locations.m_loc_nativeResolution, res, 1); + } - if (m_attributes[0].m_loc_output_resolution != -1) - { - res[0] = (float)output_res.x; - res[1] = (float)output_res.y; - m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_output_resolution, res, 1); - } - - // fragment shader - if (m_attributes[1].m_loc_texture_src_resolution != -1) - { - res[0] = (float)texture_view.baseTexture->width; - res[1] = (float)texture_view.baseTexture->height; - m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_texture_src_resolution, res, 1); - } - - if (m_attributes[1].m_loc_input_resolution != -1) - { - res[0] = (float)input_res.x; - res[1] = (float)input_res.y; - m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_input_resolution, res, 1); - } - - if (m_attributes[1].m_loc_output_resolution != -1) - { - res[0] = (float)output_res.x; - res[1] = (float)output_res.y; - m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_output_resolution, res, 1); - } + if (locations.m_loc_outputResolution != -1) + { + res[0] = (float)output_res.x; + res[1] = (float)output_res.y; + shader->SetUniform2fv(locations.m_loc_outputResolution, res, 1); + } + }; + setUniforms(m_vertex_shader, m_uniformLocations[0]); + setUniforms(m_fragment_shader, m_uniformLocations[1]); } RendererOutputShader* RendererOutputShader::s_copy_shader; @@ -341,6 +281,27 @@ void main(){ )"; return vertex_source.str(); } + +std::string RendererOutputShader::PrependFragmentPreamble(const std::string& shaderSrc) +{ + return R"(#version 430 +#ifdef VULKAN +layout(push_constant) uniform pc { + vec2 textureSrcResolution; + vec2 nativeResolution; + vec2 outputResolution; +}; +#else +uniform vec2 textureSrcResolution; +uniform vec2 nativeResolution; +uniform vec2 outputResolution; +#endif + +layout(location = 0) in vec2 passUV; +layout(binding = 0) uniform sampler2D textureSrc; +layout(location = 0) out vec4 colorOut0; +)" + shaderSrc; +} void RendererOutputShader::InitializeStatic() { std::string vertex_source, vertex_source_ud; @@ -349,28 +310,18 @@ void RendererOutputShader::InitializeStatic() { vertex_source = GetOpenGlVertexSource(false); vertex_source_ud = GetOpenGlVertexSource(true); - - s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source); - s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source); - - s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); - s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source); - - s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source); - s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source); } else { vertex_source = GetVulkanVertexSource(false); vertex_source_ud = GetVulkanVertexSource(true); - - s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source); - s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source); - - /* s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); TODO - s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source); - - s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source); - s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source);*/ } + s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source); + s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source); + + s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); + s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source); + + s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source); + s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source); } diff --git a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h index 398ac663..61b24c20 100644 --- a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h +++ b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h @@ -17,7 +17,7 @@ public: RendererOutputShader(const std::string& vertex_source, const std::string& fragment_source); virtual ~RendererOutputShader() = default; - void SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& input_res, const Vector2i& output_res) const; + void SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& output_res) const; RendererShader* GetVertexShader() const { @@ -43,16 +43,18 @@ public: static std::string GetVulkanVertexSource(bool render_upside_down); static std::string GetOpenGlVertexSource(bool render_upside_down); + static std::string PrependFragmentPreamble(const std::string& shaderSrc); + protected: RendererShader* m_vertex_shader; RendererShader* m_fragment_shader; - struct + struct UniformLocations { - sint32 m_loc_texture_src_resolution = -1; - sint32 m_loc_input_resolution = -1; - sint32 m_loc_output_resolution = -1; - } m_attributes[2]{}; + sint32 m_loc_textureSrcResolution = -1; + sint32 m_loc_nativeResolution = -1; + sint32 m_loc_outputResolution = -1; + } m_uniformLocations[2]{}; private: static const std::string s_copy_shader_source; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp index f0e2295e..de76f76d 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp @@ -202,6 +202,13 @@ VkSampler LatteTextureViewVk::GetDefaultTextureSampler(bool useLinearTexFilter) VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + // emulate OpenGL minFilters + // see note under: https://docs.vulkan.org/spec/latest/chapters/samplers.html#VkSamplerCreateInfo + // if maxLod = 0 then magnification is always performed + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 0.25f; + if (useLinearTexFilter) { samplerInfo.magFilter = VK_FILTER_LINEAR; @@ -212,6 +219,9 @@ VkSampler LatteTextureViewVk::GetDefaultTextureSampler(bool useLinearTexFilter) samplerInfo.magFilter = VK_FILTER_NEAREST; samplerInfo.minFilter = VK_FILTER_NEAREST; } + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; if (vkCreateSampler(m_device, &samplerInfo, nullptr, &sampler) != VK_SUCCESS) { diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 9ad2c5ca..37432eeb 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2581,10 +2581,18 @@ VkPipeline VulkanRenderer::backbufferBlit_createGraphicsPipeline(VkDescriptorSet colorBlending.blendConstants[2] = 0.0f; colorBlending.blendConstants[3] = 0.0f; + VkPushConstantRange pushConstantRange{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = 3 * sizeof(float) * 2 // 3 vec2's + }; + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &descriptorLayout; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; VkResult result = vkCreatePipelineLayout(m_logicalDevice, &pipelineLayoutInfo, nullptr, &m_pipelineLayout); if (result != VK_SUCCESS) @@ -2956,6 +2964,25 @@ void VulkanRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu vkCmdBindDescriptorSets(m_state.currentCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, &descriptSet, 0, nullptr); + // update push constants + Vector2f pushData[3]; + + // textureSrcResolution + sint32 effectiveWidth, effectiveHeight; + texView->baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0); + pushData[0] = {(float)effectiveWidth, (float)effectiveHeight}; + + // nativeResolution + pushData[1] = { + (float)texViewVk->baseTexture->width, + (float)texViewVk->baseTexture->height, + }; + + // outputResolution + pushData[2] = {(float)imageWidth,(float)imageHeight}; + + vkCmdPushConstants(m_state.currentCommandBuffer, m_pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float) * 2 * 3, &pushData); + vkCmdDraw(m_state.currentCommandBuffer, 6, 1, 0, 0); vkCmdEndRenderPass(m_state.currentCommandBuffer); From 2065ac5f635e48a6bf01ac480236783f46bcf0de Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 16 Nov 2024 12:51:58 +0100 Subject: [PATCH 042/137] GfxPack: Better logging messages for diagnosing problems in rules.txt --- src/Cafe/GraphicPack/GraphicPack2.cpp | 42 ++++++++++++++++----------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Cafe/GraphicPack/GraphicPack2.cpp b/src/Cafe/GraphicPack/GraphicPack2.cpp index 4b6cb095..f21bb89d 100644 --- a/src/Cafe/GraphicPack/GraphicPack2.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2.cpp @@ -345,7 +345,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) const auto preset_name = rules.FindOption("name"); if (!preset_name) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": Preset in line {} skipped because it has no name option defined", m_name, rules.GetCurrentSectionLineNumber()); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": Preset in line {} skipped because it has no name option defined", GetNormalizedPathString(), rules.GetCurrentSectionLineNumber()); continue; } @@ -369,7 +369,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) } catch (const std::exception & ex) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": Can't parse preset \"{}\": {}", m_name, *preset_name, ex.what()); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": Can't parse preset \"{}\": {}", GetNormalizedPathString(), *preset_name, ex.what()); } } else if (boost::iequals(currentSectionName, "RAM")) @@ -383,7 +383,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) { if (m_version <= 5) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": [RAM] options are only available for graphic pack version 6 or higher", m_name, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": [RAM] options are only available for graphic pack version 6 or higher", GetNormalizedPathString(), optionNameBuf); throw std::exception(); } @@ -393,12 +393,12 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) { if (addrEnd <= addrStart) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": start address (0x{:08x}) must be greater than end address (0x{:08x}) for {}", m_name, addrStart, addrEnd, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": start address (0x{:08x}) must be greater than end address (0x{:08x}) for {}", GetNormalizedPathString(), addrStart, addrEnd, optionNameBuf); throw std::exception(); } else if ((addrStart & 0xFFF) != 0 || (addrEnd & 0xFFF) != 0) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": addresses for %s are not aligned to 0x1000", m_name, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": addresses for %s are not aligned to 0x1000", GetNormalizedPathString(), optionNameBuf); throw std::exception(); } else @@ -408,7 +408,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) } else { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": has invalid syntax for option {}", m_name, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": has invalid syntax for option {}", GetNormalizedPathString(), optionNameBuf); throw std::exception(); } } @@ -422,24 +422,32 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) std::unordered_map> tmp_map; // all vars must be defined in the default preset vars before - for (const auto& entry : m_presets) + std::vector> mismatchingPresetVars; + for (const auto& presetEntry : m_presets) { - tmp_map[entry->category].emplace_back(entry); + tmp_map[presetEntry->category].emplace_back(presetEntry); - for (auto& kv : entry->variables) + for (auto& presetVar : presetEntry->variables) { - const auto it = m_preset_vars.find(kv.first); + const auto it = m_preset_vars.find(presetVar.first); if (it == m_preset_vars.cend()) { - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains preset variables which are not defined in the default section", m_name); - throw std::exception(); + mismatchingPresetVars.emplace_back(presetEntry->name, presetVar.first); + continue; } - // overwrite var type with default var type - kv.second.first = it->second.first; + presetVar.second.first = it->second.first; } } + if(!mismatchingPresetVars.empty()) + { + cemuLog_log(LogType::Force, "Graphic pack \"{}\" contains preset variables which are not defined in the [Default] section:", GetNormalizedPathString()); + for (const auto& [presetName, varName] : mismatchingPresetVars) + cemuLog_log(LogType::Force, "Preset: {} Variable: {}", presetName, varName); + throw std::exception(); + } + // have first entry be default active for every category if no default= is set for(auto entry : get_values(tmp_map)) { @@ -469,7 +477,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) auto& p2 = kv.second[i + 1]; if (p1->variables.size() != p2->variables.size()) { - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", m_name); + cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", GetNormalizedPathString()); throw std::exception(); } @@ -477,14 +485,14 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) std::set keys2(get_keys(p2->variables).begin(), get_keys(p2->variables).end()); if (keys1 != keys2) { - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", m_name); + cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", GetNormalizedPathString()); throw std::exception(); } if(p1->is_default) { if(has_default) - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" has more than one preset with the default key set for the same category \"{}\"", m_name, p1->name); + cemuLog_log(LogType::Force, "Graphic pack: \"{}\" has more than one preset with the default key set for the same category \"{}\"", GetNormalizedPathString(), p1->name); p1->active = true; has_default = true; } From c3e29fb6199cfd297a86dd0f6bc0d92fe7dab3de Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:45:33 +0100 Subject: [PATCH 043/137] Latte: Add support for shader instructions MIN_UINT and MAX_UINT Seen in the eShop version of Fatal Frame Also made some warnings less spammy since this game seems to trigger it a lot --- .../LatteDecompiler.cpp | 2 ++ .../LatteDecompilerAnalyzer.cpp | 2 ++ .../LatteDecompilerEmitGLSL.cpp | 20 +++++++++++-------- .../LatteDecompilerInstructions.h | 2 ++ src/Cafe/OS/libs/gx2/GX2_Shader.cpp | 2 +- src/input/emulated/VPADController.cpp | 2 +- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp index c3f7c19e..5972aacc 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp @@ -370,6 +370,8 @@ bool LatteDecompiler_IsALUTransInstruction(bool isOP3, uint32 opcode) opcode == ALU_OP2_INST_LSHR_INT || opcode == ALU_OP2_INST_MAX_INT || opcode == ALU_OP2_INST_MIN_INT || + opcode == ALU_OP2_INST_MAX_UINT || + opcode == ALU_OP2_INST_MIN_UINT || opcode == ALU_OP2_INST_MOVA_FLOOR || opcode == ALU_OP2_INST_MOVA_INT || opcode == ALU_OP2_INST_SETE_DX10 || diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp index 19604e0c..ff64988c 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp @@ -140,6 +140,8 @@ bool _isIntegerInstruction(const LatteDecompilerALUInstruction& aluInstruction) case ALU_OP2_INST_SUB_INT: case ALU_OP2_INST_MAX_INT: case ALU_OP2_INST_MIN_INT: + case ALU_OP2_INST_MAX_UINT: + case ALU_OP2_INST_MIN_UINT: case ALU_OP2_INST_SETE_INT: case ALU_OP2_INST_SETGT_INT: case ALU_OP2_INST_SETGE_INT: diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp index 7a6605f8..e7ebcf3a 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp @@ -1415,19 +1415,23 @@ void _emitALUOP2InstructionCode(LatteDecompilerShaderContext* shaderContext, Lat } else if( aluInstruction->opcode == ALU_OP2_INST_ADD_INT ) _emitALUOperationBinary(shaderContext, aluInstruction, " + "); - else if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MIN_INT ) + else if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MIN_INT || + aluInstruction->opcode == ALU_OP2_INST_MAX_UINT || aluInstruction->opcode == ALU_OP2_INST_MIN_UINT) { // not verified + bool isUnsigned = aluInstruction->opcode == ALU_OP2_INST_MAX_UINT || aluInstruction->opcode == ALU_OP2_INST_MIN_UINT; + auto opType = isUnsigned ? LATTE_DECOMPILER_DTYPE_UNSIGNED_INT : LATTE_DECOMPILER_DTYPE_SIGNED_INT; _emitInstructionOutputVariableName(shaderContext, aluInstruction); - if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT ) - src->add(" = max("); + src->add(" = "); + _emitTypeConversionPrefix(shaderContext, opType, outputType); + if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MAX_UINT ) + src->add("max("); else - src->add(" = min("); - _emitTypeConversionPrefix(shaderContext, LATTE_DECOMPILER_DTYPE_SIGNED_INT, outputType); - _emitOperandInputCode(shaderContext, aluInstruction, 0, LATTE_DECOMPILER_DTYPE_SIGNED_INT); + src->add("min("); + _emitOperandInputCode(shaderContext, aluInstruction, 0, opType); src->add(", "); - _emitOperandInputCode(shaderContext, aluInstruction, 1, LATTE_DECOMPILER_DTYPE_SIGNED_INT); - _emitTypeConversionSuffix(shaderContext, LATTE_DECOMPILER_DTYPE_SIGNED_INT, outputType); + _emitOperandInputCode(shaderContext, aluInstruction, 1, opType); + _emitTypeConversionSuffix(shaderContext, opType, outputType); src->add(");" _CRLF); } else if( aluInstruction->opcode == ALU_OP2_INST_SUB_INT ) diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h index 4cb1982e..6c029b46 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h @@ -60,6 +60,8 @@ #define ALU_OP2_INST_SUB_INT (0x035) // integer instruction #define ALU_OP2_INST_MAX_INT (0x036) // integer instruction #define ALU_OP2_INST_MIN_INT (0x037) // integer instruction +#define ALU_OP2_INST_MAX_UINT (0x038) // integer instruction +#define ALU_OP2_INST_MIN_UINT (0x039) // integer instruction #define ALU_OP2_INST_SETE_INT (0x03A) // integer instruction #define ALU_OP2_INST_SETGT_INT (0x03B) // integer instruction #define ALU_OP2_INST_SETGE_INT (0x03C) // integer instruction diff --git a/src/Cafe/OS/libs/gx2/GX2_Shader.cpp b/src/Cafe/OS/libs/gx2/GX2_Shader.cpp index dfbbfcff..7a153737 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Shader.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_Shader.cpp @@ -421,7 +421,7 @@ namespace GX2 { if(aluRegisterOffset&0x8000) { - cemuLog_logDebug(LogType::Force, "_GX2SubmitUniformReg(): Unhandled loop const special case or invalid offset"); + cemuLog_logDebugOnce(LogType::Force, "_GX2SubmitUniformReg(): Unhandled loop const special case or invalid offset"); return; } if((aluRegisterOffset+sizeInU32s) > 0x400) diff --git a/src/input/emulated/VPADController.cpp b/src/input/emulated/VPADController.cpp index f1ab1bc4..81615c9b 100644 --- a/src/input/emulated/VPADController.cpp +++ b/src/input/emulated/VPADController.cpp @@ -408,7 +408,7 @@ bool VPADController::push_rumble(uint8* pattern, uint8 length) std::scoped_lock lock(m_rumble_mutex); if (m_rumble_queue.size() >= 5) { - cemuLog_logDebug(LogType::Force, "too many cmds"); + cemuLog_logDebugOnce(LogType::Force, "VPADControlMotor(): Pattern too long"); return false; } From 7b513f1744a7952abf2fb927c27b2c09a2c992f4 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 17 Nov 2024 09:52:44 +0100 Subject: [PATCH 044/137] Latte: Add workaround for infinite loop in Fatal Frame shaders --- src/config/ActiveSettings.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/config/ActiveSettings.cpp b/src/config/ActiveSettings.cpp index 560f2986..f81f8336 100644 --- a/src/config/ActiveSettings.cpp +++ b/src/config/ActiveSettings.cpp @@ -198,14 +198,20 @@ bool ActiveSettings::ShaderPreventInfiniteLoopsEnabled() { const uint64 titleId = CafeSystem::GetForegroundTitleId(); // workaround for NSMBU (and variants) having a bug where shaders can get stuck in infinite loops - // update: As of Cemu 1.20.0 this should no longer be required + // Fatal Frame has an actual infinite loop in shader 0xb6a67c19f6472e00 encountered during a cutscene for the second drop (eShop version only?) + // update: As of Cemu 1.20.0 this should no longer be required for NSMBU/NSLU due to fixes with uniform handling. But we leave it here for good measure + // todo - Once we add support for loop config registers this workaround should become unnecessary return /* NSMBU JP */ titleId == 0x0005000010101C00 || /* NSMBU US */ titleId == 0x0005000010101D00 || /* NSMBU EU */ titleId == 0x0005000010101E00 || /* NSMBU+L US */ titleId == 0x000500001014B700 || /* NSMBU+L EU */ titleId == 0x000500001014B800 || /* NSLU US */ titleId == 0x0005000010142300 || - /* NSLU EU */ titleId == 0x0005000010142400; + /* NSLU EU */ titleId == 0x0005000010142400 || + /* Project Zero: Maiden of Black Water (EU) */ titleId == 0x00050000101D0300 || + /* Fatal Frame: Maiden of Black Water (US) */ titleId == 0x00050000101D0600 || + /* Project Zero: Maiden of Black Water (JP) */ titleId == 0x000500001014D200 || + /* Project Zero: Maiden of Black Water (Trial, EU) */ titleId == 0x00050000101D3F00; } bool ActiveSettings::FlushGPUCacheOnSwap() From 409f12b13a2d67f18465c2920ab04a4d5ab27ae6 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 17 Nov 2024 10:38:39 +0100 Subject: [PATCH 045/137] coreinit: Fix calculation of thread total awake time --- src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp index 2f3808b7..db457047 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp @@ -1114,13 +1114,13 @@ namespace coreinit thread->requestFlags = (OSThread_t::REQUEST_FLAG_BIT)(thread->requestFlags & OSThread_t::REQUEST_FLAG_CANCEL); // remove all flags except cancel flag // update total cycles - uint64 remainingCycles = std::min((uint64)hCPU->remainingCycles, (uint64)thread->quantumTicks); - uint64 executedCycles = thread->quantumTicks - remainingCycles; - if (executedCycles < hCPU->skippedCycles) + sint64 executedCycles = (sint64)thread->quantumTicks - (sint64)hCPU->remainingCycles; + executedCycles = std::max(executedCycles, 0); + if (executedCycles < (sint64)hCPU->skippedCycles) executedCycles = 0; else executedCycles -= hCPU->skippedCycles; - thread->totalCycles += executedCycles; + thread->totalCycles += (uint64)executedCycles; // store context and set current thread to null __OSThreadStoreContext(hCPU, thread); OSSetCurrentThread(OSGetCoreId(), nullptr); From 90eb2e01f405e502ed33fea330352e167c4a14db Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Fri, 22 Nov 2024 12:43:12 +0000 Subject: [PATCH 046/137] nsyshid/dimensions: add missing return (#1425) --- src/Cafe/OS/libs/nsyshid/Dimensions.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cafe/OS/libs/nsyshid/Dimensions.cpp b/src/Cafe/OS/libs/nsyshid/Dimensions.cpp index f328dde7..8a2acc76 100644 --- a/src/Cafe/OS/libs/nsyshid/Dimensions.cpp +++ b/src/Cafe/OS/libs/nsyshid/Dimensions.cpp @@ -675,6 +675,7 @@ namespace nsyshid figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); m_figureAddedRemovedResponses.push(figureChangeResponse); + return true; } bool DimensionsUSB::CancelRemove(uint8 index) From 073523768692c1adb994ce2f07d1c29530de41e6 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sat, 30 Nov 2024 22:05:50 +0000 Subject: [PATCH 047/137] Input: Move pairing dialog button and source (#1424) --- src/gui/CMakeLists.txt | 4 ++-- src/gui/input/InputSettings2.cpp | 9 +++++++++ src/gui/{ => input}/PairingDialog.cpp | 2 +- src/gui/{ => input}/PairingDialog.h | 0 src/gui/input/panels/WiimoteInputPanel.cpp | 12 ------------ 5 files changed, 12 insertions(+), 15 deletions(-) rename src/gui/{ => input}/PairingDialog.cpp (99%) rename src/gui/{ => input}/PairingDialog.h (100%) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 02f96a9c..e1a04ec0 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -75,6 +75,8 @@ add_library(CemuGui input/InputAPIAddWindow.h input/InputSettings2.cpp input/InputSettings2.h + input/PairingDialog.cpp + input/PairingDialog.h input/panels/ClassicControllerInputPanel.cpp input/panels/ClassicControllerInputPanel.h input/panels/InputPanel.cpp @@ -97,8 +99,6 @@ add_library(CemuGui MemorySearcherTool.h PadViewFrame.cpp PadViewFrame.h - PairingDialog.cpp - PairingDialog.h TitleManager.cpp TitleManager.h EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp diff --git a/src/gui/input/InputSettings2.cpp b/src/gui/input/InputSettings2.cpp index 72bf4f7d..2ae8a74b 100644 --- a/src/gui/input/InputSettings2.cpp +++ b/src/gui/input/InputSettings2.cpp @@ -20,6 +20,8 @@ #include "gui/input/InputAPIAddWindow.h" #include "input/ControllerFactory.h" +#include "gui/input/PairingDialog.h" + #include "gui/input/panels/VPADInputPanel.h" #include "gui/input/panels/ProControllerInputPanel.h" @@ -252,6 +254,13 @@ wxWindow* InputSettings2::initialize_page(size_t index) page_data.m_controller_api_remove = remove_api; } + auto* pairingDialog = new wxButton(page, wxID_ANY, _("Pair Wii/Wii U Controller")); + pairingDialog->Bind(wxEVT_BUTTON, [this](wxEvent&) { + PairingDialog pairing_dialog(this); + pairing_dialog.ShowModal(); + }); + sizer->Add(pairingDialog, wxGBPosition(5, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); + // controller auto* controller_bttns = new wxBoxSizer(wxHORIZONTAL); auto* settings = new wxButton(page, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0); diff --git a/src/gui/PairingDialog.cpp b/src/gui/input/PairingDialog.cpp similarity index 99% rename from src/gui/PairingDialog.cpp rename to src/gui/input/PairingDialog.cpp index f90e6d13..03d6315b 100644 --- a/src/gui/PairingDialog.cpp +++ b/src/gui/input/PairingDialog.cpp @@ -1,5 +1,5 @@ #include "gui/wxgui.h" -#include "gui/PairingDialog.h" +#include "PairingDialog.h" #if BOOST_OS_WINDOWS #include diff --git a/src/gui/PairingDialog.h b/src/gui/input/PairingDialog.h similarity index 100% rename from src/gui/PairingDialog.h rename to src/gui/input/PairingDialog.h diff --git a/src/gui/input/panels/WiimoteInputPanel.cpp b/src/gui/input/panels/WiimoteInputPanel.cpp index 050baad1..44a7c001 100644 --- a/src/gui/input/panels/WiimoteInputPanel.cpp +++ b/src/gui/input/panels/WiimoteInputPanel.cpp @@ -12,7 +12,6 @@ #include "input/emulated/WiimoteController.h" #include "gui/helpers/wxHelpers.h" #include "gui/components/wxInputDraw.h" -#include "gui/PairingDialog.h" constexpr WiimoteController::ButtonId g_kFirstColumnItems[] = { @@ -40,11 +39,6 @@ WiimoteInputPanel::WiimoteInputPanel(wxWindow* parent) auto* main_sizer = new wxBoxSizer(wxVERTICAL); auto* horiz_main_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* pair_button = new wxButton(this, wxID_ANY, _("Pair a Wii or Wii U controller")); - pair_button->Bind(wxEVT_BUTTON, &WiimoteInputPanel::on_pair_button, this); - horiz_main_sizer->Add(pair_button); - horiz_main_sizer->AddSpacer(10); - auto* extensions_sizer = new wxBoxSizer(wxHORIZONTAL); horiz_main_sizer->Add(extensions_sizer, wxSizerFlags(0).Align(wxALIGN_CENTER_VERTICAL)); @@ -264,9 +258,3 @@ void WiimoteInputPanel::load_controller(const EmulatedControllerPtr& emulated_co set_active_device_type(wiimote->get_device_type()); } } - -void WiimoteInputPanel::on_pair_button(wxCommandEvent& event) -{ - PairingDialog pairing_dialog(this); - pairing_dialog.ShowModal(); -} From 80a6057512240b14f6aa6ab45ba955e25a1c7acf Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Mon, 2 Dec 2024 01:01:22 +0100 Subject: [PATCH 048/137] build: Fix linker failure with glslang 15.0.0 (#1436) --- src/Cafe/CMakeLists.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 0901fece..76dba007 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -532,6 +532,12 @@ set_property(TARGET CemuCafe PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$ Date: Sun, 1 Dec 2024 20:19:15 -0800 Subject: [PATCH 049/137] Set version for macOS bundle (#1431) --- src/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3792ab85..79471321 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,8 +82,8 @@ if (MACOS_BUNDLE) set(MACOSX_BUNDLE_ICON_FILE "cemu.icns") set(MACOSX_BUNDLE_GUI_IDENTIFIER "info.cemu.Cemu") set(MACOSX_BUNDLE_BUNDLE_NAME "Cemu") - set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${CMAKE_PROJECT_VERSION}) - set(MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION}) + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") set(MACOSX_BUNDLE_COPYRIGHT "Copyright © 2024 Cemu Project") set(MACOSX_BUNDLE_CATEGORY "public.app-category.games") From 40d9664d1c23a7a1ff6e0197a77a4358032b870a Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Sat, 7 Dec 2024 07:14:20 +0000 Subject: [PATCH 050/137] Update translation files --- bin/resources/ar/â€â€cemu.mo | Bin 0 -> 81849 bytes bin/resources/de/cemu.mo | Bin 65571 -> 69412 bytes bin/resources/ru/cemu.mo | Bin 90752 -> 91888 bytes bin/resources/sv/cemu.mo | Bin 15821 -> 67980 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 bin/resources/ar/â€â€cemu.mo diff --git a/bin/resources/ar/â€â€cemu.mo b/bin/resources/ar/â€â€cemu.mo new file mode 100644 index 0000000000000000000000000000000000000000..4062628be1486a0ffe6a0d2731c6853e4b133880 GIT binary patch literal 81849 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 Vgzj&`LH}xLZ2dy=?u{P|{|}7B5uN}5 literal 0 HcmV?d00001 diff --git a/bin/resources/de/cemu.mo b/bin/resources/de/cemu.mo index 8dc4e8cc9ec032d5dcd38af5359b3246398a7e0a..cd9edd3cb981068219006dd4b84921e7b6169b00 100644 GIT binary patch delta 25224 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 delta 21408 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 diff --git a/bin/resources/ru/cemu.mo b/bin/resources/ru/cemu.mo index 4ff04e2bf7ba65f396cd0c2f640e8bc7df3c35cf..eb8f372f691d87c2d6059a630432007c5ee44eaa 100644 GIT binary patch delta 19385 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 O^1lV&H7d##==wj(F5LbA delta 18537 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 literal 15821 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

?) zKO}?KTd7oOGxV67B%Rz@smM)mIPM&tFcq?U6@#+L<4sB9ZRc4f@+Nne+^)*xF0y}K zbsx#Ra#S_Ryx5zO&Lgv;2H7~Lw%%vY4RJOd?15&Jh2`N020>mezsYe!& z`OyAHwl2#8Hj{uHTq!J*4r|QwADstmsB3y6&Z#b?s)yfGb-i(}}EovM%$O%%imz*HphY+VdBiJaJVgbEn z(t(coPTjKJ$u5*T!Bsmgzer+`BBEEXIySjGCc4(`?R7GSqy||V=q+v9zDG*1OpY)q zXGubqAnAz7-Q-31n}SzzCa*1fG-C3Ecw3VwoKZ%$XT3=jlDT)*vzM}Sv$?oqflOy= zsxTp&Oe1aft&E#&gN)=<$ zL)O~Z|m2S<**9`r0n?x zPkF^`PeK7+NVpkuMhX)z`&LpB3z8kku{N9>Kc^UqtcT2E#cBM=5{r$io_5_<DGgMPL!q0r8RT-g3fzy0rUqXXZvWplS*+qAu+srfe)7n(4 zQ<`xvrPv+LAuM=tffX6^=aWH?R1!w*C*?R8H{t1;P%EG>G^*pK={(zq`5PFIyGfht zYCYD6z``WGRP;dK`VZLu`CZk)i;l zE|sdZVM2p5!-e)RVdn2fwz8F|-%G#7}a+~W= zX_zGZeI<1^mCz09nC`H;n|G?rUruSvGHFa_HSQ9Xxyt+Fv9#)$VztVd6|X^%E$=W+ zxU61g-6E$sZmo>rq(vLcwH}b2oVAa-!wF)GcEhnP$=$YUXltTXXv~i!2`F3R6L%T&LpLGY**IHbIxY7piy>EM)1Rb1LbFBT zmdTwy>n%JygSVD3Ed$D%+zz9XTP3xlIEu^|lUt&1{29HJc6V&V`KhZcvt}lAxb2ad z$21rzJx<;b>ow<6wl1eF{tB?GRGf;)6$}D7cOKM-W0cLRH@W% ztlp24jzTB9X~L*C%S`6j!lX7rbJ-eFl@|P)9NN-Q&r=%Cq-17#W0IE%_Bfe0IW7Mz`H_F&tt5!U3gISr3gL zHlb?z$ExHgtRs}BN`Dq;WL0ZkC;DkdvHYE#jZ%KueDO4YNZ`9Yr^Y?pzT$+T23 zWH~aeD6>|1SZ9{KWK|W7HnXZ5JaUF?XpIdjRANf{YzMrxIrZDd!?tT&ep+$`OYx21PwRF$~f^{3phZk{aLTn>LmvB$iqsQ;5Pq^LtZ(=HusWb zZ5&fkfDQAEbt=o6ZPVSfuk}C9OikYyOzS2#H zypSFw)FA3@9iCN0zp)e=7;OuAnh)y*{sy6BToq^5{xW|&@r|05ef!9kb2M(+c3p8@%TeMY z7iLM3%u#N($=-83XLa2y2Nqcy)_>p3>E%w+{wla4EU%>soUR$tF-g12yEvR2+D zd?!~?*XE?-g0Hb8BUQId^ehAqz~S zz2#}=)ueextEc5X3U=MfnprH7-SiXEl@-4{B`G|!%J!V_@2yrbx2=^-ueJbQsXC0* I$@Jv>FAgnol>h($ From e2d0871ca34ab9c20bd9939e661c299067b04124 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 7 Dec 2024 07:48:30 +0100 Subject: [PATCH 051/137] Camera: Set error code in CAMInit Fixes Hunter's Trophy 2 crashing on boot --- src/Cafe/OS/libs/camera/camera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 4debb37f..03e01bfc 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -181,7 +181,7 @@ namespace camera sint32 CAMInit(uint32 cameraId, CAMInitInfo_t* camInitInfo, uint32be* error) { CameraInstance* camInstance = new CameraInstance(camInitInfo->width, camInitInfo->height, camInitInfo->handlerFuncPtr); - + *error = 0; // Hunter's Trophy 2 will fail to boot if we don't set this std::unique_lock _lock(g_mutex_camera); if (g_cameraCounter == 0) { From 356cf0e5e00a17a632ee6c845072a877c96c8eaf Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 7 Dec 2024 09:23:11 +0100 Subject: [PATCH 052/137] Multiple smaller HLE improvements --- src/Cafe/OS/RPL/rpl_structs.h | 2 +- src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp | 14 +++++++++++++- src/Cafe/OS/libs/coreinit/coreinit_GHS.h | 4 ++++ src/Cafe/OS/libs/coreinit/coreinit_Thread.h | 2 +- src/Cafe/OS/libs/gx2/GX2_Resource.cpp | 6 ++++++ src/Cafe/OS/libs/nsysnet/nsysnet.cpp | 11 +++-------- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Cafe/OS/RPL/rpl_structs.h b/src/Cafe/OS/RPL/rpl_structs.h index 998ec8d7..c66f6136 100644 --- a/src/Cafe/OS/RPL/rpl_structs.h +++ b/src/Cafe/OS/RPL/rpl_structs.h @@ -116,7 +116,7 @@ typedef struct /* +0x34 */ uint32be ukn34; /* +0x38 */ uint32be ukn38; /* +0x3C */ uint32be ukn3C; - /* +0x40 */ uint32be toolkitVersion; + /* +0x40 */ uint32be minimumToolkitVersion; /* +0x44 */ uint32be ukn44; /* +0x48 */ uint32be ukn48; /* +0x4C */ uint32be ukn4C; diff --git a/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp b/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp index e2864fb9..33c8eedc 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp @@ -156,12 +156,22 @@ namespace coreinit return ¤tThread->crt.eh_mem_manage; } - void* __gh_errno_ptr() + sint32be* __gh_errno_ptr() { OSThread_t* currentThread = coreinit::OSGetCurrentThread(); return ¤tThread->context.ghs_errno; } + void __gh_set_errno(sint32 errNo) + { + *__gh_errno_ptr() = errNo; + } + + sint32 __gh_get_errno() + { + return *__gh_errno_ptr(); + } + void* __get_eh_store_globals() { OSThread_t* currentThread = coreinit::OSGetCurrentThread(); @@ -272,6 +282,8 @@ namespace coreinit cafeExportRegister("coreinit", __get_eh_globals, LogType::Placeholder); cafeExportRegister("coreinit", __get_eh_mem_manage, LogType::Placeholder); cafeExportRegister("coreinit", __gh_errno_ptr, LogType::Placeholder); + cafeExportRegister("coreinit", __gh_set_errno, LogType::Placeholder); + cafeExportRegister("coreinit", __gh_get_errno, LogType::Placeholder); cafeExportRegister("coreinit", __get_eh_store_globals, LogType::Placeholder); cafeExportRegister("coreinit", __get_eh_store_globals_tdeh, LogType::Placeholder); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_GHS.h b/src/Cafe/OS/libs/coreinit/coreinit_GHS.h index 0ac09e94..5f000732 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_GHS.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_GHS.h @@ -4,5 +4,9 @@ namespace coreinit { void PrepareGHSRuntime(); + sint32be* __gh_errno_ptr(); + void __gh_set_errno(sint32 errNo); + sint32 __gh_get_errno(); + void InitializeGHS(); }; \ No newline at end of file diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h index df787bf0..1a93022b 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h @@ -38,7 +38,7 @@ struct OSContext_t /* +0x1E0 */ uint64be fp_ps1[32]; /* +0x2E0 */ uint64be coretime[3]; /* +0x2F8 */ uint64be starttime; - /* +0x300 */ uint32be ghs_errno; // returned by __gh_errno_ptr() (used by socketlasterr) + /* +0x300 */ sint32be ghs_errno; // returned by __gh_errno_ptr() (used by socketlasterr) /* +0x304 */ uint32be affinity; /* +0x308 */ uint32be upmc1; /* +0x30C */ uint32be upmc2; diff --git a/src/Cafe/OS/libs/gx2/GX2_Resource.cpp b/src/Cafe/OS/libs/gx2/GX2_Resource.cpp index 97f51a0d..a6029de9 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Resource.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_Resource.cpp @@ -87,6 +87,11 @@ namespace GX2 return true; } + void GX2RSetBufferName(GX2RBuffer* buffer, const char* name) + { + // no-op in production builds + } + void* GX2RLockBufferEx(GX2RBuffer* buffer, uint32 resFlags) { return buffer->GetPtr(); @@ -226,6 +231,7 @@ namespace GX2 cafeExportRegister("gx2", GX2RCreateBufferUserMemory, LogType::GX2); cafeExportRegister("gx2", GX2RDestroyBufferEx, LogType::GX2); cafeExportRegister("gx2", GX2RBufferExists, LogType::GX2); + cafeExportRegister("gx2", GX2RSetBufferName, LogType::GX2); cafeExportRegister("gx2", GX2RLockBufferEx, LogType::GX2); cafeExportRegister("gx2", GX2RUnlockBufferEx, LogType::GX2); cafeExportRegister("gx2", GX2RInvalidateBuffer, LogType::GX2); diff --git a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp index 5a0ddc59..c83915db 100644 --- a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp +++ b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp @@ -3,6 +3,7 @@ #include "Cafe/OS/libs/coreinit/coreinit_Thread.h" #include "Cafe/IOSU/legacy/iosu_crypto.h" #include "Cafe/OS/libs/coreinit/coreinit_Time.h" +#include "Cafe/OS/libs/coreinit/coreinit_GHS.h" #include "Common/socket.h" @@ -117,20 +118,14 @@ void nsysnetExport_socket_lib_finish(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, 0); // 0 -> Success } -static uint32be* __gh_errno_ptr() -{ - OSThread_t* osThread = coreinit::OSGetCurrentThread(); - return &osThread->context.ghs_errno; -} - void _setSockError(sint32 errCode) { - *(uint32be*)__gh_errno_ptr() = (uint32)errCode; + coreinit::__gh_set_errno(errCode); } sint32 _getSockError() { - return (sint32)*(uint32be*)__gh_errno_ptr(); + return coreinit::__gh_get_errno(); } // error translation modes for _translateError From 934cb5460577dd4f672ae3d9c918826e50073934 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 7 Dec 2024 09:24:59 +0100 Subject: [PATCH 053/137] Properly check if MLC is writeable --- src/gui/CemuApp.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index 50ff3b89..c4b1f4e4 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -234,6 +234,12 @@ void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) g_config.Save(); } } + else + { + // default path is not writeable. Just let the user know and quit. Unsure if it would be a good idea to ask the user to choose an alternative path instead + wxMessageBox(formatWxString(_("Cemu failed to write to the default mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); + } } bool CemuApp::OnInit() @@ -507,6 +513,13 @@ bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc) file.flush(); file.close(); } + // create a dummy file in the mlc folder to check if it's writable + const auto dummyFile = fs::path(mlc).append("writetestdummy"); + std::ofstream file(dummyFile); + if (!file.is_open()) + return false; + file.close(); + fs::remove(dummyFile); } catch (const std::exception& ex) { From dd0af0a56fa3c6b8a82f60c19e67bbe06d673d0e Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sat, 7 Dec 2024 11:02:40 +0000 Subject: [PATCH 054/137] Linux: Allow connecting Wiimotes via L2CAP (#1353) --- .github/workflows/build.yml | 4 +- BUILD.md | 6 +- CMakeLists.txt | 7 + cmake/Findbluez.cmake | 20 + src/gui/input/PairingDialog.cpp | 394 ++++++++++-------- src/gui/input/PairingDialog.h | 2 +- src/input/CMakeLists.txt | 10 + .../api/Wiimote/WiimoteControllerProvider.cpp | 120 ++++-- .../api/Wiimote/WiimoteControllerProvider.h | 6 +- src/input/api/Wiimote/WiimoteDevice.h | 3 +- .../api/Wiimote/hidapi/HidapiWiimote.cpp | 7 +- src/input/api/Wiimote/hidapi/HidapiWiimote.h | 4 +- src/input/api/Wiimote/l2cap/L2CapWiimote.cpp | 148 +++++++ src/input/api/Wiimote/l2cap/L2CapWiimote.h | 22 + 14 files changed, 532 insertions(+), 221 deletions(-) create mode 100644 cmake/Findbluez.cmake create mode 100644 src/input/api/Wiimote/l2cap/L2CapWiimote.cpp create mode 100644 src/input/api/Wiimote/l2cap/L2CapWiimote.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72bbcf52..6ae4b892 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: - name: "Install system dependencies" run: | sudo apt update -qq - sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev nasm ninja-build + sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev nasm ninja-build libbluetooth-dev - name: "Setup cmake" uses: jwlawson/actions-setup-cmake@v2 @@ -96,7 +96,7 @@ jobs: - name: "Install system dependencies" run: | sudo apt update -qq - sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev nasm ninja-build appstream + sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev nasm ninja-build appstream libbluetooth-dev - name: "Build AppImage" run: | diff --git a/BUILD.md b/BUILD.md index 44d69c6c..41de928e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -46,10 +46,10 @@ To compile Cemu, a recent enough compiler and STL with C++20 support is required ### Dependencies #### For Arch and derivatives: -`sudo pacman -S --needed base-devel clang cmake freeglut git glm gtk3 libgcrypt libpulse libsecret linux-headers llvm nasm ninja systemd unzip zip` +`sudo pacman -S --needed base-devel bluez-libs clang cmake freeglut git glm gtk3 libgcrypt libpulse libsecret linux-headers llvm nasm ninja systemd unzip zip` #### For Debian, Ubuntu and derivatives: -`sudo apt install -y cmake curl clang-15 freeglut3-dev git libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libtool nasm ninja-build` +`sudo apt install -y cmake curl clang-15 freeglut3-dev git libbluetooth-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libtool nasm ninja-build` You may also need to install `libusb-1.0-0-dev` as a workaround for an issue with the vcpkg hidapi package. @@ -57,7 +57,7 @@ At Step 3 in [Build Cemu using cmake and clang](#build-cemu-using-cmake-and-clan `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DCMAKE_C_COMPILER=/usr/bin/clang-15 -DCMAKE_CXX_COMPILER=/usr/bin/clang++-15 -G Ninja -DCMAKE_MAKE_PROGRAM=/usr/bin/ninja` #### For Fedora and derivatives: -`sudo dnf install clang cmake cubeb-devel freeglut-devel git glm-devel gtk3-devel kernel-headers libgcrypt-devel libsecret-devel libtool libusb1-devel llvm nasm ninja-build perl-core systemd-devel zlib-devel zlib-static` +`sudo dnf install bluez-libs clang cmake cubeb-devel freeglut-devel git glm-devel gtk3-devel kernel-headers libgcrypt-devel libsecret-devel libtool libusb1-devel llvm nasm ninja-build perl-core systemd-devel zlib-devel zlib-static` ### Build Cemu diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b5cff6c..cf04b235 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ endif() if (UNIX AND NOT APPLE) option(ENABLE_WAYLAND "Build with Wayland support" ON) option(ENABLE_FERAL_GAMEMODE "Enables Feral Interactive GameMode Support" ON) + option(ENABLE_BLUEZ "Build with Bluez support" ON) endif() option(ENABLE_OPENGL "Enables the OpenGL backend" ON) @@ -179,6 +180,12 @@ if (UNIX AND NOT APPLE) endif() find_package(GTK3 REQUIRED) + if(ENABLE_BLUEZ) + find_package(bluez REQUIRED) + set(ENABLE_WIIMOTE ON) + add_compile_definitions(HAS_BLUEZ) + endif() + endif() if (ENABLE_VULKAN) diff --git a/cmake/Findbluez.cmake b/cmake/Findbluez.cmake new file mode 100644 index 00000000..007cdac9 --- /dev/null +++ b/cmake/Findbluez.cmake @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 Andrea Pappacoda +# SPDX-License-Identifier: ISC + +find_package(bluez CONFIG) +if (NOT bluez_FOUND) + find_package(PkgConfig) + if (PKG_CONFIG_FOUND) + pkg_search_module(bluez IMPORTED_TARGET GLOBAL bluez-1.0 bluez) + if (bluez_FOUND) + add_library(bluez::bluez ALIAS PkgConfig::bluez) + endif () + endif () +endif () + +find_package_handle_standard_args(bluez + REQUIRED_VARS + bluez_LINK_LIBRARIES + bluez_FOUND + VERSION_VAR bluez_VERSION +) diff --git a/src/gui/input/PairingDialog.cpp b/src/gui/input/PairingDialog.cpp index 03d6315b..350fce81 100644 --- a/src/gui/input/PairingDialog.cpp +++ b/src/gui/input/PairingDialog.cpp @@ -4,233 +4,297 @@ #if BOOST_OS_WINDOWS #include #endif +#if BOOST_OS_LINUX +#include +#include +#include +#include +#endif wxDECLARE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); wxDEFINE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); PairingDialog::PairingDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Pairing..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) + : wxDialog(parent, wxID_ANY, _("Pairing..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) { - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(350, 20), wxGA_HORIZONTAL); - m_gauge->SetValue(0); - sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); + auto* sizer = new wxBoxSizer(wxVERTICAL); + m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(350, 20), wxGA_HORIZONTAL); + m_gauge->SetValue(0); + sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); - auto* rows = new wxFlexGridSizer(0, 2, 0, 0); - rows->AddGrowableCol(1); + auto* rows = new wxFlexGridSizer(0, 2, 0, 0); + rows->AddGrowableCol(1); - m_text = new wxStaticText(this, wxID_ANY, _("Searching for controllers...")); - rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + m_text = new wxStaticText(this, wxID_ANY, _("Searching for controllers...")); + rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - { - auto* right_side = new wxBoxSizer(wxHORIZONTAL); + { + auto* right_side = new wxBoxSizer(wxHORIZONTAL); - m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancelButton->Bind(wxEVT_BUTTON, &PairingDialog::OnCancelButton, this); - right_side->Add(m_cancelButton, 0, wxALL, 5); + m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); + m_cancelButton->Bind(wxEVT_BUTTON, &PairingDialog::OnCancelButton, this); + right_side->Add(m_cancelButton, 0, wxALL, 5); - rows->Add(right_side, 1, wxALIGN_RIGHT, 5); - } + rows->Add(right_side, 1, wxALIGN_RIGHT, 5); + } - sizer->Add(rows, 0, wxALL | wxEXPAND, 5); + sizer->Add(rows, 0, wxALL | wxEXPAND, 5); - SetSizerAndFit(sizer); - Centre(wxBOTH); + SetSizerAndFit(sizer); + Centre(wxBOTH); - Bind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); - Bind(wxEVT_PROGRESS_PAIR, &PairingDialog::OnGaugeUpdate, this); + Bind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); + Bind(wxEVT_PROGRESS_PAIR, &PairingDialog::OnGaugeUpdate, this); - m_thread = std::thread(&PairingDialog::WorkerThread, this); + m_thread = std::thread(&PairingDialog::WorkerThread, this); } PairingDialog::~PairingDialog() { - Unbind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); + Unbind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); } void PairingDialog::OnClose(wxCloseEvent& event) { - event.Skip(); + event.Skip(); - m_threadShouldQuit = true; - if (m_thread.joinable()) - m_thread.join(); + m_threadShouldQuit = true; + if (m_thread.joinable()) + m_thread.join(); } void PairingDialog::OnCancelButton(const wxCommandEvent& event) { - Close(); + Close(); } void PairingDialog::OnGaugeUpdate(wxCommandEvent& event) { - PairingState state = (PairingState)event.GetInt(); + PairingState state = (PairingState)event.GetInt(); - switch (state) - { - case PairingState::Pairing: - { - m_text->SetLabel(_("Found controller. Pairing...")); - m_gauge->SetValue(50); - break; - } + switch (state) + { + case PairingState::Pairing: + { + m_text->SetLabel(_("Found controller. Pairing...")); + m_gauge->SetValue(50); + break; + } - case PairingState::Finished: - { - m_text->SetLabel(_("Successfully paired the controller.")); - m_gauge->SetValue(100); - m_cancelButton->SetLabel(_("Close")); - break; - } + case PairingState::Finished: + { + m_text->SetLabel(_("Successfully paired the controller.")); + m_gauge->SetValue(100); + m_cancelButton->SetLabel(_("Close")); + break; + } - case PairingState::NoBluetoothAvailable: - { - m_text->SetLabel(_("Failed to find a suitable Bluetooth radio.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } + case PairingState::NoBluetoothAvailable: + { + m_text->SetLabel(_("Failed to find a suitable Bluetooth radio.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } - case PairingState::BluetoothFailed: - { - m_text->SetLabel(_("Failed to search for controllers.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } + case PairingState::SearchFailed: + { + m_text->SetLabel(_("Failed to find controllers.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } - case PairingState::PairingFailed: - { - m_text->SetLabel(_("Failed to pair with the found controller.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } + case PairingState::PairingFailed: + { + m_text->SetLabel(_("Failed to pair with the found controller.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } - case PairingState::BluetoothUnusable: - { - m_text->SetLabel(_("Please use your system's Bluetooth manager instead.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } + case PairingState::BluetoothUnusable: + { + m_text->SetLabel(_("Please use your system's Bluetooth manager instead.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } - - default: - { - break; - } - } + default: + { + break; + } + } } -void PairingDialog::WorkerThread() -{ - const std::wstring wiimoteName = L"Nintendo RVL-CNT-01"; - const std::wstring wiiUProControllerName = L"Nintendo RVL-CNT-01-UC"; - #if BOOST_OS_WINDOWS - const GUID bthHidGuid = {0x00001124,0x0000,0x1000,{0x80,0x00,0x00,0x80,0x5F,0x9B,0x34,0xFB}}; +void PairingDialog::WorkerThread() +{ + const std::wstring wiimoteName = L"Nintendo RVL-CNT-01"; + const std::wstring wiiUProControllerName = L"Nintendo RVL-CNT-01-UC"; - const BLUETOOTH_FIND_RADIO_PARAMS radioFindParams = - { - .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS) - }; + const GUID bthHidGuid = {0x00001124, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}}; - HANDLE radio = INVALID_HANDLE_VALUE; - HBLUETOOTH_RADIO_FIND radioFind = BluetoothFindFirstRadio(&radioFindParams, &radio); - if (radioFind == nullptr) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } + const BLUETOOTH_FIND_RADIO_PARAMS radioFindParams = + { + .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS)}; - BluetoothFindRadioClose(radioFind); + HANDLE radio = INVALID_HANDLE_VALUE; + HBLUETOOTH_RADIO_FIND radioFind = BluetoothFindFirstRadio(&radioFindParams, &radio); + if (radioFind == nullptr) + { + UpdateCallback(PairingState::NoBluetoothAvailable); + return; + } - BLUETOOTH_RADIO_INFO radioInfo = - { - .dwSize = sizeof(BLUETOOTH_RADIO_INFO) - }; + BluetoothFindRadioClose(radioFind); - DWORD result = BluetoothGetRadioInfo(radio, &radioInfo); - if (result != ERROR_SUCCESS) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } + BLUETOOTH_RADIO_INFO radioInfo = + { + .dwSize = sizeof(BLUETOOTH_RADIO_INFO)}; - const BLUETOOTH_DEVICE_SEARCH_PARAMS searchParams = - { - .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS), + DWORD result = BluetoothGetRadioInfo(radio, &radioInfo); + if (result != ERROR_SUCCESS) + { + UpdateCallback(PairingState::NoBluetoothAvailable); + return; + } - .fReturnAuthenticated = FALSE, - .fReturnRemembered = FALSE, - .fReturnUnknown = TRUE, - .fReturnConnected = FALSE, + const BLUETOOTH_DEVICE_SEARCH_PARAMS searchParams = + { + .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS), - .fIssueInquiry = TRUE, - .cTimeoutMultiplier = 5, + .fReturnAuthenticated = FALSE, + .fReturnRemembered = FALSE, + .fReturnUnknown = TRUE, + .fReturnConnected = FALSE, - .hRadio = radio - }; + .fIssueInquiry = TRUE, + .cTimeoutMultiplier = 5, - BLUETOOTH_DEVICE_INFO info = - { - .dwSize = sizeof(BLUETOOTH_DEVICE_INFO) - }; + .hRadio = radio}; - while (!m_threadShouldQuit) - { - HBLUETOOTH_DEVICE_FIND deviceFind = BluetoothFindFirstDevice(&searchParams, &info); - if (deviceFind == nullptr) - { - UpdateCallback(PairingState::BluetoothFailed); - return; - } + BLUETOOTH_DEVICE_INFO info = + { + .dwSize = sizeof(BLUETOOTH_DEVICE_INFO)}; - while (!m_threadShouldQuit) - { - if (info.szName == wiimoteName || info.szName == wiiUProControllerName) - { - BluetoothFindDeviceClose(deviceFind); + while (!m_threadShouldQuit) + { + HBLUETOOTH_DEVICE_FIND deviceFind = BluetoothFindFirstDevice(&searchParams, &info); + if (deviceFind == nullptr) + { + UpdateCallback(PairingState::SearchFailed); + return; + } - UpdateCallback(PairingState::Pairing); + while (!m_threadShouldQuit) + { + if (info.szName == wiimoteName || info.szName == wiiUProControllerName) + { + BluetoothFindDeviceClose(deviceFind); - wchar_t passwd[6] = { radioInfo.address.rgBytes[0], radioInfo.address.rgBytes[1], radioInfo.address.rgBytes[2], radioInfo.address.rgBytes[3], radioInfo.address.rgBytes[4], radioInfo.address.rgBytes[5] }; - DWORD bthResult = BluetoothAuthenticateDevice(nullptr, radio, &info, passwd, 6); - if (bthResult != ERROR_SUCCESS) - { - UpdateCallback(PairingState::PairingFailed); - return; - } + UpdateCallback(PairingState::Pairing); - bthResult = BluetoothSetServiceState(radio, &info, &bthHidGuid, BLUETOOTH_SERVICE_ENABLE); - if (bthResult != ERROR_SUCCESS) - { - UpdateCallback(PairingState::PairingFailed); - return; - } + wchar_t passwd[6] = {radioInfo.address.rgBytes[0], radioInfo.address.rgBytes[1], radioInfo.address.rgBytes[2], radioInfo.address.rgBytes[3], radioInfo.address.rgBytes[4], radioInfo.address.rgBytes[5]}; + DWORD bthResult = BluetoothAuthenticateDevice(nullptr, radio, &info, passwd, 6); + if (bthResult != ERROR_SUCCESS) + { + UpdateCallback(PairingState::PairingFailed); + return; + } - UpdateCallback(PairingState::Finished); - return; - } + bthResult = BluetoothSetServiceState(radio, &info, &bthHidGuid, BLUETOOTH_SERVICE_ENABLE); + if (bthResult != ERROR_SUCCESS) + { + UpdateCallback(PairingState::PairingFailed); + return; + } - BOOL nextDevResult = BluetoothFindNextDevice(deviceFind, &info); - if (nextDevResult == FALSE) - { - break; - } - } + UpdateCallback(PairingState::Finished); + return; + } - BluetoothFindDeviceClose(deviceFind); - } -#else - UpdateCallback(PairingState::BluetoothUnusable); -#endif + BOOL nextDevResult = BluetoothFindNextDevice(deviceFind, &info); + if (nextDevResult == FALSE) + { + break; + } + } + + BluetoothFindDeviceClose(deviceFind); + } } +#elif BOOST_OS_LINUX +void PairingDialog::WorkerThread() +{ + constexpr static uint8_t LIAC_LAP[] = {0x00, 0x8b, 0x9e}; + constexpr static auto isWiimoteName = [](std::string_view name) { + return name == "Nintendo RVL-CNT-01" || name == "Nintendo RVL-CNT-01-TR"; + }; + + // Get default BT device + const auto hostId = hci_get_route(nullptr); + if (hostId < 0) + { + UpdateCallback(PairingState::NoBluetoothAvailable); + return; + } + + // Search for device + inquiry_info* infos = nullptr; + m_cancelButton->Disable(); + const auto respCount = hci_inquiry(hostId, 7, 4, LIAC_LAP, &infos, IREQ_CACHE_FLUSH); + m_cancelButton->Enable(); + if (respCount <= 0) + { + UpdateCallback(PairingState::SearchFailed); + return; + } + stdx::scope_exit infoFree([&]() { bt_free(infos);}); + + if (m_threadShouldQuit) + return; + + // Open dev to read name + const auto hostDev = hci_open_dev(hostId); + stdx::scope_exit devClose([&]() { hci_close_dev(hostDev);}); + + char nameBuffer[HCI_MAX_NAME_LENGTH] = {}; + + bool foundADevice = false; + // Get device name and compare. Would use product and vendor id from SDP, but many third-party Wiimotes don't store them + for (const auto& devInfo : std::span(infos, respCount)) + { + const auto& addr = devInfo.bdaddr; + const auto err = hci_read_remote_name(hostDev, &addr, HCI_MAX_NAME_LENGTH, nameBuffer, + 2000); + if (m_threadShouldQuit) + return; + if (err || !isWiimoteName(nameBuffer)) + continue; + + L2CapWiimote::AddCandidateAddress(addr); + foundADevice = true; + const auto& b = addr.b; + cemuLog_log(LogType::Force, "Pairing Dialog: Found '{}' with address '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'", + nameBuffer, b[5], b[4], b[3], b[2], b[1], b[0]); + } + if (foundADevice) + UpdateCallback(PairingState::Finished); + else + UpdateCallback(PairingState::SearchFailed); +} +#else +void PairingDialog::WorkerThread() +{ + UpdateCallback(PairingState::BluetoothUnusable); +} +#endif void PairingDialog::UpdateCallback(PairingState state) { - auto* event = new wxCommandEvent(wxEVT_PROGRESS_PAIR); - event->SetInt((int)state); - wxQueueEvent(this, event); + auto* event = new wxCommandEvent(wxEVT_PROGRESS_PAIR); + event->SetInt((int)state); + wxQueueEvent(this, event); } \ No newline at end of file diff --git a/src/gui/input/PairingDialog.h b/src/gui/input/PairingDialog.h index 6c7612d1..02cab4fc 100644 --- a/src/gui/input/PairingDialog.h +++ b/src/gui/input/PairingDialog.h @@ -17,7 +17,7 @@ private: Pairing, Finished, NoBluetoothAvailable, - BluetoothFailed, + SearchFailed, PairingFailed, BluetoothUnusable }; diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt index 9f7873a1..004dc2ba 100644 --- a/src/input/CMakeLists.txt +++ b/src/input/CMakeLists.txt @@ -73,6 +73,11 @@ if (ENABLE_WIIMOTE) api/Wiimote/hidapi/HidapiWiimote.cpp api/Wiimote/hidapi/HidapiWiimote.h ) + if (UNIX AND NOT APPLE) + target_sources(CemuInput PRIVATE + api/Wiimote/l2cap/L2CapWiimote.cpp + api/Wiimote/l2cap/L2CapWiimote.h) + endif() endif () @@ -97,3 +102,8 @@ endif() if (ENABLE_WXWIDGETS) target_link_libraries(CemuInput PRIVATE wx::base wx::core) endif() + + +if (UNIX AND NOT APPLE) + target_link_libraries(CemuInput PRIVATE bluez::bluez) +endif () \ No newline at end of file diff --git a/src/input/api/Wiimote/WiimoteControllerProvider.cpp b/src/input/api/Wiimote/WiimoteControllerProvider.cpp index c80f3fbe..221d75a7 100644 --- a/src/input/api/Wiimote/WiimoteControllerProvider.cpp +++ b/src/input/api/Wiimote/WiimoteControllerProvider.cpp @@ -2,7 +2,12 @@ #include "input/api/Wiimote/NativeWiimoteController.h" #include "input/api/Wiimote/WiimoteMessages.h" +#ifdef HAS_HIDAPI #include "input/api/Wiimote/hidapi/HidapiWiimote.h" +#endif +#ifdef HAS_BLUEZ +#include "input/api/Wiimote/l2cap/L2CapWiimote.h" +#endif #include #include @@ -12,6 +17,7 @@ WiimoteControllerProvider::WiimoteControllerProvider() { m_reader_thread = std::thread(&WiimoteControllerProvider::reader_thread, this); m_writer_thread = std::thread(&WiimoteControllerProvider::writer_thread, this); + m_connectionThread = std::thread(&WiimoteControllerProvider::connectionThread, this); } WiimoteControllerProvider::~WiimoteControllerProvider() @@ -21,48 +27,51 @@ WiimoteControllerProvider::~WiimoteControllerProvider() m_running = false; m_writer_thread.join(); m_reader_thread.join(); + m_connectionThread.join(); } } std::vector> WiimoteControllerProvider::get_controllers() { + m_connectedDeviceMutex.lock(); + auto devices = m_connectedDevices; + m_connectedDeviceMutex.unlock(); + std::scoped_lock lock(m_device_mutex); - std::queue disconnected_wiimote_indices; - for (auto i{0u}; i < m_wiimotes.size(); ++i){ - if (!(m_wiimotes[i].connected = m_wiimotes[i].device->write_data({kStatusRequest, 0x00}))){ - disconnected_wiimote_indices.push(i); - } - } - - const auto valid_new_device = [&](std::shared_ptr & device) { - const auto writeable = device->write_data({kStatusRequest, 0x00}); - const auto not_already_connected = - std::none_of(m_wiimotes.cbegin(), m_wiimotes.cend(), - [device](const auto& it) { - return (*it.device == *device) && it.connected; - }); - return writeable && not_already_connected; - }; - - for (auto& device : WiimoteDevice_t::get_devices()) + for (auto& device : devices) { - if (!valid_new_device(device)) + const auto writeable = device->write_data({kStatusRequest, 0x00}); + if (!writeable) continue; - // Replace disconnected wiimotes - if (!disconnected_wiimote_indices.empty()){ - const auto idx = disconnected_wiimote_indices.front(); - disconnected_wiimote_indices.pop(); - m_wiimotes.replace(idx, std::make_unique(device)); - } - // Otherwise add them - else { - m_wiimotes.push_back(std::make_unique(device)); - } + bool isDuplicate = false; + ssize_t lowestReplaceableIndex = -1; + for (ssize_t i = m_wiimotes.size() - 1; i >= 0; --i) + { + const auto& wiimoteDevice = m_wiimotes[i].device; + if (wiimoteDevice) + { + if (*wiimoteDevice == *device) + { + isDuplicate = true; + break; + } + continue; + } + + lowestReplaceableIndex = i; + } + if (isDuplicate) + continue; + if (lowestReplaceableIndex != -1) + m_wiimotes.replace(lowestReplaceableIndex, std::make_unique(device)); + else + m_wiimotes.push_back(std::make_unique(device)); } std::vector> result; + result.reserve(m_wiimotes.size()); for (size_t i = 0; i < m_wiimotes.size(); ++i) { result.emplace_back(std::make_shared(i)); @@ -74,7 +83,7 @@ std::vector> WiimoteControllerProvider::get_cont bool WiimoteControllerProvider::is_connected(size_t index) { std::shared_lock lock(m_device_mutex); - return index < m_wiimotes.size() && m_wiimotes[index].connected; + return index < m_wiimotes.size() && m_wiimotes[index].device; } bool WiimoteControllerProvider::is_registered_device(size_t index) @@ -141,6 +150,30 @@ WiimoteControllerProvider::WiimoteState WiimoteControllerProvider::get_state(siz return {}; } +void WiimoteControllerProvider::connectionThread() +{ + SetThreadName("Wiimote-connect"); + while (m_running.load(std::memory_order_relaxed)) + { + std::vector devices; +#ifdef HAS_HIDAPI + const auto& hidDevices = HidapiWiimote::get_devices(); + std::ranges::move(hidDevices, std::back_inserter(devices)); +#endif +#ifdef HAS_BLUEZ + const auto& l2capDevices = L2CapWiimote::get_devices(); + std::ranges::move(l2capDevices, std::back_inserter(devices)); +#endif + { + std::scoped_lock lock(m_connectedDeviceMutex); + m_connectedDevices.clear(); + std::ranges::move(devices, std::back_inserter(m_connectedDevices)); + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } +} + + void WiimoteControllerProvider::reader_thread() { SetThreadName("Wiimote-reader"); @@ -148,7 +181,7 @@ void WiimoteControllerProvider::reader_thread() while (m_running.load(std::memory_order_relaxed)) { const auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - lastCheck) > std::chrono::seconds(2)) + if (std::chrono::duration_cast(now - lastCheck) > std::chrono::milliseconds(500)) { // check for new connected wiimotes get_controllers(); @@ -160,11 +193,16 @@ void WiimoteControllerProvider::reader_thread() for (size_t index = 0; index < m_wiimotes.size(); ++index) { auto& wiimote = m_wiimotes[index]; - if (!wiimote.connected) + if (!wiimote.device) continue; const auto read_data = wiimote.device->read_data(); - if (!read_data || read_data->empty()) + if (!read_data) + { + wiimote.device.reset(); + continue; + } + if (read_data->empty()) continue; receivedAnyPacket = true; @@ -921,18 +959,18 @@ void WiimoteControllerProvider::writer_thread() if (index != (size_t)-1 && !data.empty()) { - if (m_wiimotes[index].rumble) + auto& wiimote = m_wiimotes[index]; + if (!wiimote.device) + continue; + if (wiimote.rumble) data[1] |= 1; - - m_wiimotes[index].connected = m_wiimotes[index].device->write_data(data); - if (m_wiimotes[index].connected) + if (!wiimote.device->write_data(data)) { - m_wiimotes[index].data_ts = std::chrono::high_resolution_clock::now(); + wiimote.device.reset(); + wiimote.rumble = false; } else - { - m_wiimotes[index].rumble = false; - } + wiimote.data_ts = std::chrono::high_resolution_clock::now(); } device_lock.unlock(); diff --git a/src/input/api/Wiimote/WiimoteControllerProvider.h b/src/input/api/Wiimote/WiimoteControllerProvider.h index 7629b641..90f28d5c 100644 --- a/src/input/api/Wiimote/WiimoteControllerProvider.h +++ b/src/input/api/Wiimote/WiimoteControllerProvider.h @@ -77,16 +77,17 @@ public: private: std::atomic_bool m_running = false; std::thread m_reader_thread, m_writer_thread; - std::shared_mutex m_device_mutex; + std::thread m_connectionThread; + std::vector m_connectedDevices; + std::mutex m_connectedDeviceMutex; struct Wiimote { Wiimote(WiimoteDevicePtr device) : device(std::move(device)) {} WiimoteDevicePtr device; - std::atomic_bool connected = true; std::atomic_bool rumble = false; std::shared_mutex mutex; @@ -103,6 +104,7 @@ private: void reader_thread(); void writer_thread(); + void connectionThread(); void calibrate(size_t index); IRMode set_ir_camera(size_t index, bool state); diff --git a/src/input/api/Wiimote/WiimoteDevice.h b/src/input/api/Wiimote/WiimoteDevice.h index 7938bbdf..8ea5b321 100644 --- a/src/input/api/Wiimote/WiimoteDevice.h +++ b/src/input/api/Wiimote/WiimoteDevice.h @@ -9,8 +9,7 @@ public: virtual bool write_data(const std::vector& data) = 0; virtual std::optional> read_data() = 0; - virtual bool operator==(WiimoteDevice& o) const = 0; - bool operator!=(WiimoteDevice& o) const { return *this == o; } + virtual bool operator==(const WiimoteDevice& o) const = 0; }; using WiimoteDevicePtr = std::shared_ptr; diff --git a/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp b/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp index db185675..5780909f 100644 --- a/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp +++ b/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp @@ -47,8 +47,11 @@ std::vector HidapiWiimote::get_devices() { return wiimote_devices; } -bool HidapiWiimote::operator==(WiimoteDevice& o) const { - return static_cast(o).m_path == m_path; +bool HidapiWiimote::operator==(const WiimoteDevice& rhs) const { + auto other = dynamic_cast(&rhs); + if (!other) + return false; + return m_path == other->m_path; } HidapiWiimote::~HidapiWiimote() { diff --git a/src/input/api/Wiimote/hidapi/HidapiWiimote.h b/src/input/api/Wiimote/hidapi/HidapiWiimote.h index 858cb1f3..952a36f0 100644 --- a/src/input/api/Wiimote/hidapi/HidapiWiimote.h +++ b/src/input/api/Wiimote/hidapi/HidapiWiimote.h @@ -10,7 +10,7 @@ public: bool write_data(const std::vector &data) override; std::optional> read_data() override; - bool operator==(WiimoteDevice& o) const override; + bool operator==(const WiimoteDevice& o) const override; static std::vector get_devices(); @@ -19,5 +19,3 @@ private: const std::string m_path; }; - -using WiimoteDevice_t = HidapiWiimote; \ No newline at end of file diff --git a/src/input/api/Wiimote/l2cap/L2CapWiimote.cpp b/src/input/api/Wiimote/l2cap/L2CapWiimote.cpp new file mode 100644 index 00000000..28a123f3 --- /dev/null +++ b/src/input/api/Wiimote/l2cap/L2CapWiimote.cpp @@ -0,0 +1,148 @@ +#include "L2CapWiimote.h" +#include + +constexpr auto comparator = [](const bdaddr_t& a, const bdaddr_t& b) { + return bacmp(&a, &b); +}; + +static auto s_addresses = std::map(comparator); +static std::mutex s_addressMutex; + +static bool AttemptConnect(int sockFd, const sockaddr_l2& addr) +{ + auto res = connect(sockFd, reinterpret_cast(&addr), + sizeof(sockaddr_l2)); + if (res == 0) + return true; + return connect(sockFd, reinterpret_cast(&addr), + sizeof(sockaddr_l2)) == 0; +} + +static bool AttemptSetNonBlock(int sockFd) +{ + return fcntl(sockFd, F_SETFL, fcntl(sockFd, F_GETFL) | O_NONBLOCK) == 0; +} + +L2CapWiimote::L2CapWiimote(int recvFd, int sendFd, bdaddr_t addr) + : m_recvFd(recvFd), m_sendFd(sendFd), m_addr(addr) +{ +} + +L2CapWiimote::~L2CapWiimote() +{ + close(m_recvFd); + close(m_sendFd); + const auto& b = m_addr.b; + cemuLog_logDebug(LogType::Force, "Wiimote at {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} disconnected", b[5], b[4], b[3], b[2], b[1], b[0]); + + // Re-add to candidate vec + s_addressMutex.lock(); + s_addresses[m_addr] = false; + s_addressMutex.unlock(); +} + +void L2CapWiimote::AddCandidateAddress(bdaddr_t addr) +{ + std::scoped_lock lock(s_addressMutex); + s_addresses.try_emplace(addr, false); +} + +std::vector L2CapWiimote::get_devices() +{ + s_addressMutex.lock(); + std::vector unconnected; + for (const auto& [addr, connected] : s_addresses) + { + if (!connected) + unconnected.push_back(addr); + } + s_addressMutex.unlock(); + + std::vector outDevices; + for (const auto& addr : unconnected) + { + // Socket for sending data to controller, PSM 0x11 + auto sendFd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (sendFd < 0) + { + cemuLog_logDebug(LogType::Force, "Failed to open send socket: {}", strerror(errno)); + continue; + } + + sockaddr_l2 sendAddr{}; + sendAddr.l2_family = AF_BLUETOOTH; + sendAddr.l2_psm = htobs(0x11); + sendAddr.l2_bdaddr = addr; + + if (!AttemptConnect(sendFd, sendAddr) || !AttemptSetNonBlock(sendFd)) + { + const auto& b = addr.b; + cemuLog_logDebug(LogType::Force, "Failed to connect send socket to '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}': {}", + b[5], b[4], b[3], b[2], b[1], b[0], strerror(errno)); + close(sendFd); + continue; + } + + // Socket for receiving data from controller, PSM 0x13 + auto recvFd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (recvFd < 0) + { + cemuLog_logDebug(LogType::Force, "Failed to open recv socket: {}", strerror(errno)); + close(sendFd); + continue; + } + sockaddr_l2 recvAddr{}; + recvAddr.l2_family = AF_BLUETOOTH; + recvAddr.l2_psm = htobs(0x13); + recvAddr.l2_bdaddr = addr; + + if (!AttemptConnect(recvFd, recvAddr) || !AttemptSetNonBlock(recvFd)) + { + const auto& b = addr.b; + cemuLog_logDebug(LogType::Force, "Failed to connect recv socket to '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}': {}", + b[5], b[4], b[3], b[2], b[1], b[0], strerror(errno)); + close(sendFd); + close(recvFd); + continue; + } + outDevices.emplace_back(std::make_shared(sendFd, recvFd, addr)); + + s_addressMutex.lock(); + s_addresses[addr] = true; + s_addressMutex.unlock(); + } + return outDevices; +} + +bool L2CapWiimote::write_data(const std::vector& data) +{ + const auto size = data.size(); + cemu_assert_debug(size < 23); + uint8 buffer[23]; + // All outgoing messages must be prefixed with 0xA2 + buffer[0] = 0xA2; + std::memcpy(buffer + 1, data.data(), size); + const auto outSize = size + 1; + return send(m_sendFd, buffer, outSize, 0) == outSize; +} + +std::optional> L2CapWiimote::read_data() +{ + uint8 buffer[23]; + const auto nBytes = recv(m_sendFd, buffer, 23, 0); + + if (nBytes < 0 && errno == EWOULDBLOCK) + return std::vector{}; + // All incoming messages must be prefixed with 0xA1 + if (nBytes < 2 || buffer[0] != 0xA1) + return std::nullopt; + return std::vector(buffer + 1, buffer + 1 + nBytes - 1); +} + +bool L2CapWiimote::operator==(const WiimoteDevice& rhs) const +{ + auto mote = dynamic_cast(&rhs); + if (!mote) + return false; + return bacmp(&m_addr, &mote->m_addr) == 0; +} \ No newline at end of file diff --git a/src/input/api/Wiimote/l2cap/L2CapWiimote.h b/src/input/api/Wiimote/l2cap/L2CapWiimote.h new file mode 100644 index 00000000..cc8d071b --- /dev/null +++ b/src/input/api/Wiimote/l2cap/L2CapWiimote.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include + +class L2CapWiimote : public WiimoteDevice +{ + public: + L2CapWiimote(int recvFd, int sendFd, bdaddr_t addr); + ~L2CapWiimote() override; + + bool write_data(const std::vector& data) override; + std::optional> read_data() override; + bool operator==(const WiimoteDevice& o) const override; + + static void AddCandidateAddress(bdaddr_t addr); + static std::vector get_devices(); + private: + int m_recvFd; + int m_sendFd; + bdaddr_t m_addr; +}; + From adab729f43ca27bc81280be764b32f11a350308e Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 8 Dec 2024 13:33:34 +0100 Subject: [PATCH 055/137] UI: Correctly handle unicode paths during save export --- src/gui/TitleManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/TitleManager.cpp b/src/gui/TitleManager.cpp index 00e7992f..4a4f7f56 100644 --- a/src/gui/TitleManager.cpp +++ b/src/gui/TitleManager.cpp @@ -632,7 +632,7 @@ void TitleManager::OnSaveExport(wxCommandEvent& event) const auto persistent_id = (uint32)(uintptr_t)m_save_account_list->GetClientData(selection_index); - wxFileDialog path_dialog(this, _("Select a target file to export the save entry"), entry->path.string(), wxEmptyString, + wxFileDialog path_dialog(this, _("Select a target file to export the save entry"), wxHelper::FromPath(entry->path), wxEmptyString, fmt::format("{}|*.zip", _("Exported save entry (*.zip)")), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().IsEmpty()) return; From 6aaad1eb83819bcfb9109da2e5e09b1f8d776722 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:47:05 +0100 Subject: [PATCH 056/137] Debugger: Added right click context menu to disasm view + small fixes --- src/Cafe/HW/Espresso/Debugger/Debugger.cpp | 28 +++++++ src/Cafe/HW/Espresso/Debugger/Debugger.h | 1 + src/gui/debugger/DebuggerWindow2.cpp | 11 ++- src/gui/debugger/DebuggerWindow2.h | 2 + src/gui/debugger/DisasmCtrl.cpp | 86 +++++++++++++++++----- src/gui/debugger/DisasmCtrl.h | 13 ++++ 6 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp index e7369af6..1fed07cd 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp @@ -447,6 +447,34 @@ bool debugger_hasPatch(uint32 address) return false; } +void debugger_removePatch(uint32 address) +{ + for (sint32 i = 0; i < debuggerState.patches.size(); i++) + { + auto& patch = debuggerState.patches[i]; + if (address < patch->address || address >= (patch->address + patch->length)) + continue; + MPTR startAddress = patch->address; + MPTR endAddress = patch->address + patch->length; + // remove any breakpoints overlapping with the patch + for (auto& bp : debuggerState.breakpoints) + { + if (bp->address + 4 > startAddress && bp->address < endAddress) + { + bp->enabled = false; + debugger_updateExecutionBreakpoint(bp->address); + } + } + // restore original data + memcpy(MEMPTR(startAddress).GetPtr(), patch->origData.data(), patch->length); + PPCRecompiler_invalidateRange(startAddress, endAddress); + // remove patch + delete patch; + debuggerState.patches.erase(debuggerState.patches.begin() + i); + return; + } +} + void debugger_stepInto(PPCInterpreter_t* hCPU, bool updateDebuggerWindow = true) { bool isRecEnabled = ppcRecompilerEnabled; diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.h b/src/Cafe/HW/Espresso/Debugger/Debugger.h index 717df28a..249c47b8 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.h +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.h @@ -114,6 +114,7 @@ void debugger_updateExecutionBreakpoint(uint32 address, bool forceRestore = fals void debugger_createPatch(uint32 address, std::span patchData); bool debugger_hasPatch(uint32 address); +void debugger_removePatch(uint32 address); void debugger_forceBreak(); // force breakpoint at the next possible instruction bool debugger_isTrapped(); diff --git a/src/gui/debugger/DebuggerWindow2.cpp b/src/gui/debugger/DebuggerWindow2.cpp index 969e40bd..9f25cf96 100644 --- a/src/gui/debugger/DebuggerWindow2.cpp +++ b/src/gui/debugger/DebuggerWindow2.cpp @@ -64,6 +64,7 @@ wxBEGIN_EVENT_TABLE(DebuggerWindow2, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_RUN, DebuggerWindow2::OnRunProgram) EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_LOADED, DebuggerWindow2::OnNotifyModuleLoaded) EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_UNLOADED, DebuggerWindow2::OnNotifyModuleUnloaded) + EVT_COMMAND(wxID_ANY, wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, DebuggerWindow2::OnDisasmCtrlGotoAddress) // file menu EVT_MENU(MENU_ID_FILE_EXIT, DebuggerWindow2::OnExit) // window @@ -383,6 +384,12 @@ void DebuggerWindow2::OnMoveIP(wxCommandEvent& event) m_disasm_ctrl->CenterOffset(ip); } +void DebuggerWindow2::OnDisasmCtrlGotoAddress(wxCommandEvent& event) +{ + uint32 address = static_cast(event.GetExtraLong()); + UpdateModuleLabel(address); +} + void DebuggerWindow2::OnParentMove(const wxPoint& main_position, const wxSize& main_size) { m_main_position = main_position; @@ -416,7 +423,7 @@ void DebuggerWindow2::OnNotifyModuleLoaded(wxCommandEvent& event) void DebuggerWindow2::OnNotifyModuleUnloaded(wxCommandEvent& event) { - RPLModule* module = (RPLModule*)event.GetClientData(); + RPLModule* module = (RPLModule*)event.GetClientData(); // todo - the RPL module is already unloaded at this point. Find a better way to handle this SaveModuleStorage(module, true); m_module_window->OnGameLoaded(); m_symbol_window->OnGameLoaded(); @@ -659,7 +666,7 @@ void DebuggerWindow2::CreateMenuBar() void DebuggerWindow2::UpdateModuleLabel(uint32 address) { - if(address == 0) + if (address == 0) address = m_disasm_ctrl->GetViewBaseAddress(); RPLModule* module = RPLLoader_FindModuleByCodeAddr(address); diff --git a/src/gui/debugger/DebuggerWindow2.h b/src/gui/debugger/DebuggerWindow2.h index 0ca44c44..145b5e1d 100644 --- a/src/gui/debugger/DebuggerWindow2.h +++ b/src/gui/debugger/DebuggerWindow2.h @@ -86,6 +86,8 @@ private: void OnMoveIP(wxCommandEvent& event); void OnNotifyModuleLoaded(wxCommandEvent& event); void OnNotifyModuleUnloaded(wxCommandEvent& event); + // events from DisasmCtrl + void OnDisasmCtrlGotoAddress(wxCommandEvent& event); void CreateMenuBar(); void UpdateModuleLabel(uint32 address = 0); diff --git a/src/gui/debugger/DisasmCtrl.cpp b/src/gui/debugger/DisasmCtrl.cpp index c2cd5722..2f38d55e 100644 --- a/src/gui/debugger/DisasmCtrl.cpp +++ b/src/gui/debugger/DisasmCtrl.cpp @@ -15,6 +15,8 @@ #include "Cafe/HW/Espresso/Debugger/DebugSymbolStorage.h" #include // for wxMemoryInputStream +wxDEFINE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent); + #define MAX_SYMBOL_LEN (120) #define COLOR_DEBUG_ACTIVE_BP 0xFFFFA0FF @@ -74,6 +76,8 @@ DisasmCtrl::DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& po auto tooltip_sizer = new wxBoxSizer(wxVERTICAL); tooltip_sizer->Add(new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString), 0, wxALL, 5); m_tooltip_window->SetSizer(tooltip_sizer); + + Bind(wxEVT_MENU, &DisasmCtrl::OnContextMenuEntryClicked, this, IDContextMenu_ToggleBreakpoint, IDContextMenu_Last); } void DisasmCtrl::Init() @@ -662,29 +666,67 @@ void DisasmCtrl::CopyToClipboard(std::string text) { #endif } +static uint32 GetUnrelocatedAddress(MPTR address) +{ + RPLModule* rplModule = RPLLoader_FindModuleByCodeAddr(address); + if (!rplModule) + return 0; + if (address >= rplModule->regionMappingBase_text.GetMPTR() && address < (rplModule->regionMappingBase_text.GetMPTR() + rplModule->regionSize_text)) + return 0x02000000 + (address - rplModule->regionMappingBase_text.GetMPTR()); + return 0; +} + void DisasmCtrl::OnContextMenu(const wxPoint& position, uint32 line) { - wxPoint pos = position; auto optVirtualAddress = LinePixelPosToAddress(position.y - GetViewStart().y * m_line_height); if (!optVirtualAddress) return; MPTR virtualAddress = *optVirtualAddress; + m_contextMenuAddress = virtualAddress; + // show dialog + wxMenu menu; + menu.Append(IDContextMenu_ToggleBreakpoint, _("Toggle breakpoint")); + if(debugger_hasPatch(virtualAddress)) + menu.Append(IDContextMenu_RestoreOriginalInstructions, _("Restore original instructions")); + menu.AppendSeparator(); + menu.Append(IDContextMenu_CopyAddress, _("Copy address")); + uint32 unrelocatedAddress = GetUnrelocatedAddress(virtualAddress); + if (unrelocatedAddress && unrelocatedAddress != virtualAddress) + menu.Append(IDContextMenu_CopyUnrelocatedAddress, _("Copy virtual address (for IDA/Ghidra)")); + PopupMenu(&menu); +} - // address - if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE) +void DisasmCtrl::OnContextMenuEntryClicked(wxCommandEvent& event) +{ + switch(event.GetId()) { - CopyToClipboard(fmt::format("{:#10x}", virtualAddress)); - return; - } - else if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE + OFFSET_DISASSEMBLY) - { - // double-clicked on disassembly (operation and operand data) - return; - } - else - { - // comment - return; + case IDContextMenu_ToggleBreakpoint: + { + debugger_toggleExecuteBreakpoint(m_contextMenuAddress); + wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); + wxPostEvent(this->m_parent, evt); + break; + } + case IDContextMenu_RestoreOriginalInstructions: + { + debugger_removePatch(m_contextMenuAddress); + wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); // This also refreshes the disassembly view + wxPostEvent(this->m_parent, evt); + break; + } + case IDContextMenu_CopyAddress: + { + CopyToClipboard(fmt::format("{:#10x}", m_contextMenuAddress)); + break; + } + case IDContextMenu_CopyUnrelocatedAddress: + { + uint32 unrelocatedAddress = GetUnrelocatedAddress(m_contextMenuAddress); + CopyToClipboard(fmt::format("{:#10x}", unrelocatedAddress)); + break; + } + default: + UNREACHABLE; } } @@ -722,7 +764,6 @@ std::optional DisasmCtrl::LinePixelPosToAddress(sint32 posY) if (posY < 0) return std::nullopt; - sint32 lineIndex = posY / m_line_height; if (lineIndex >= m_lineToAddress.size()) return std::nullopt; @@ -751,8 +792,6 @@ void DisasmCtrl::CenterOffset(uint32 offset) m_active_line = line; RefreshLine(m_active_line); - - debug_printf("scroll to %x\n", debuggerState.debugSession.instructionPointer); } void DisasmCtrl::GoToAddressDialog() @@ -765,6 +804,10 @@ void DisasmCtrl::GoToAddressDialog() auto value = goto_dialog.GetValue().ToStdString(); std::transform(value.begin(), value.end(), value.begin(), tolower); + // trim any leading spaces + while(!value.empty() && value[0] == ' ') + value.erase(value.begin()); + debugger_addParserSymbols(parser); // try to parse expression as hex value first (it should interpret 1234 as 0x1234, not 1234) @@ -773,17 +816,24 @@ void DisasmCtrl::GoToAddressDialog() const auto result = (uint32)parser.Evaluate("0x"+value); m_lastGotoTarget = result; CenterOffset(result); + wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS); + evt.SetExtraLong(static_cast(result)); + wxPostEvent(GetParent(), evt); } else if (parser.IsConstantExpression(value)) { const auto result = (uint32)parser.Evaluate(value); m_lastGotoTarget = result; CenterOffset(result); + wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS); + evt.SetExtraLong(static_cast(result)); + wxPostEvent(GetParent(), evt); } else { try { + // if not a constant expression (i.e. relying on unknown variables), then evaluating will throw an exception with a detailed error message const auto _ = (uint32)parser.Evaluate(value); } catch (const std::exception& ex) diff --git a/src/gui/debugger/DisasmCtrl.h b/src/gui/debugger/DisasmCtrl.h index 993d5697..5a67e49a 100644 --- a/src/gui/debugger/DisasmCtrl.h +++ b/src/gui/debugger/DisasmCtrl.h @@ -1,9 +1,20 @@ #pragma once #include "gui/components/TextList.h" +wxDECLARE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent); // Notify parent that goto address operation completed. Event contains the address that was jumped to. + class DisasmCtrl : public TextList { + enum + { + IDContextMenu_ToggleBreakpoint = wxID_HIGHEST + 1, + IDContextMenu_RestoreOriginalInstructions, + IDContextMenu_CopyAddress, + IDContextMenu_CopyUnrelocatedAddress, + IDContextMenu_Last + }; public: + DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style); void Init(); @@ -26,6 +37,7 @@ protected: void OnKeyPressed(sint32 key_code, const wxPoint& position) override; void OnMouseDClick(const wxPoint& position, uint32 line) override; void OnContextMenu(const wxPoint& position, uint32 line) override; + void OnContextMenuEntryClicked(wxCommandEvent& event); bool OnShowTooltip(const wxPoint& position, uint32 line) override; void ScrollWindow(int dx, int dy, const wxRect* prect) override; @@ -40,6 +52,7 @@ private: sint32 m_mouse_line, m_mouse_line_drawn; sint32 m_active_line; uint32 m_lastGotoTarget{}; + uint32 m_contextMenuAddress{}; // code region info uint32 currentCodeRegionStart; uint32 currentCodeRegionEnd; From b53b223ba9b45974cd80674d8de9c6e736e34ae9 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:38:43 +0100 Subject: [PATCH 057/137] Vulkan: Use cache for sampler objects --- .../HW/Latte/Core/LattePerformanceMonitor.h | 1 + src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h | 48 ++++++-- .../Latte/Renderer/Vulkan/VulkanRenderer.cpp | 110 +++++++++++++++++- .../Renderer/Vulkan/VulkanRendererCore.cpp | 8 +- 4 files changed, 152 insertions(+), 15 deletions(-) diff --git a/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h b/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h index 713e094e..ac75bb1b 100644 --- a/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h +++ b/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h @@ -124,6 +124,7 @@ typedef struct LattePerfStatCounter numGraphicPipelines; LattePerfStatCounter numImages; LattePerfStatCounter numImageViews; + LattePerfStatCounter numSamplers; LattePerfStatCounter numRenderPass; LattePerfStatCounter numFramebuffer; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h b/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h index f79bd2dc..9c7e03f3 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h @@ -19,7 +19,7 @@ public: virtual ~VKRMoveableRefCounter() { - cemu_assert_debug(refCount == 0); + cemu_assert_debug(m_refCount == 0); // remove references #ifdef CEMU_DEBUG_ASSERT @@ -30,7 +30,11 @@ public: } #endif for (auto itr : refs) - itr->ref->refCount--; + { + itr->ref->m_refCount--; + if (itr->ref->m_refCount == 0) + itr->ref->RefCountReachedZero(); + } refs.clear(); delete selfRef; selfRef = nullptr; @@ -41,8 +45,8 @@ public: VKRMoveableRefCounter(VKRMoveableRefCounter&& rhs) noexcept { this->refs = std::move(rhs.refs); - this->refCount = rhs.refCount; - rhs.refCount = 0; + this->m_refCount = rhs.m_refCount; + rhs.m_refCount = 0; this->selfRef = rhs.selfRef; rhs.selfRef = nullptr; this->selfRef->ref = this; @@ -57,7 +61,7 @@ public: void addRef(VKRMoveableRefCounter* refTarget) { this->refs.emplace_back(refTarget->selfRef); - refTarget->refCount++; + refTarget->m_refCount++; #ifdef CEMU_DEBUG_ASSERT // add reverse ref @@ -68,16 +72,23 @@ public: // methods to directly increment/decrement ref counter (for situations where no external object is available) void incRef() { - this->refCount++; + m_refCount++; } void decRef() { - this->refCount--; + m_refCount--; + if (m_refCount == 0) + RefCountReachedZero(); } protected: - int refCount{}; + virtual void RefCountReachedZero() + { + // does nothing by default + } + + int m_refCount{}; private: VKRMoveableRefCounterRef* selfRef; std::vector refs; @@ -88,7 +99,7 @@ private: void moveObj(VKRMoveableRefCounter&& rhs) { this->refs = std::move(rhs.refs); - this->refCount = rhs.refCount; + this->m_refCount = rhs.m_refCount; this->selfRef = rhs.selfRef; this->selfRef->ref = this; } @@ -131,6 +142,25 @@ public: VkSampler m_textureDefaultSampler[2] = { VK_NULL_HANDLE, VK_NULL_HANDLE }; // relict from LatteTextureViewVk, get rid of it eventually }; + +class VKRObjectSampler : public VKRDestructibleObject +{ + public: + VKRObjectSampler(VkSamplerCreateInfo* samplerInfo); + ~VKRObjectSampler() override; + + static VKRObjectSampler* GetOrCreateSampler(VkSamplerCreateInfo* samplerInfo); + static void DestroyCache(); + + void RefCountReachedZero() override; // sampler objects are destroyed when not referenced anymore + + VkSampler GetSampler() const { return m_sampler; } + private: + static std::unordered_map s_samplerCache; + VkSampler m_sampler{ VK_NULL_HANDLE }; + uint64 m_hash; +}; + class VKRObjectRenderPass : public VKRDestructibleObject { public: diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 37432eeb..eae6daf2 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -672,6 +672,8 @@ VulkanRenderer::~VulkanRenderer() if (m_commandPool != VK_NULL_HANDLE) vkDestroyCommandPool(m_logicalDevice, m_commandPool, nullptr); + VKRObjectSampler::DestroyCache(); + // destroy debug callback if (m_debugCallback) { @@ -3707,6 +3709,7 @@ void VulkanRenderer::AppendOverlayDebugInfo() ImGui::Text("DS StorageBuf %u", performanceMonitor.vk.numDescriptorStorageBuffers.get()); ImGui::Text("Images %u", performanceMonitor.vk.numImages.get()); ImGui::Text("ImageView %u", performanceMonitor.vk.numImageViews.get()); + ImGui::Text("ImageSampler %u", performanceMonitor.vk.numSamplers.get()); ImGui::Text("RenderPass %u", performanceMonitor.vk.numRenderPass.get()); ImGui::Text("Framebuffer %u", performanceMonitor.vk.numFramebuffer.get()); m_spinlockDestructionQueue.lock(); @@ -3752,7 +3755,7 @@ void VKRDestructibleObject::flagForCurrentCommandBuffer() bool VKRDestructibleObject::canDestroy() { - if (refCount > 0) + if (m_refCount > 0) return false; return VulkanRenderer::GetInstance()->HasCommandBufferFinished(m_lastCmdBufferId); } @@ -3793,6 +3796,111 @@ VKRObjectTextureView::~VKRObjectTextureView() performanceMonitor.vk.numImageViews.decrement(); } +static uint64 CalcHashSamplerCreateInfo(const VkSamplerCreateInfo& info) +{ + uint64 h = 0xcbf29ce484222325ULL; + auto fnvHashCombine = [](uint64_t &h, auto val) { + using T = decltype(val); + static_assert(sizeof(T) <= 8); + uint64_t val64 = 0; + std::memcpy(&val64, &val, sizeof(val)); + h ^= val64; + h *= 0x100000001b3ULL; + }; + cemu_assert_debug(info.sType == VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO); + fnvHashCombine(h, info.flags); + fnvHashCombine(h, info.magFilter); + fnvHashCombine(h, info.minFilter); + fnvHashCombine(h, info.mipmapMode); + fnvHashCombine(h, info.addressModeU); + fnvHashCombine(h, info.addressModeV); + fnvHashCombine(h, info.addressModeW); + fnvHashCombine(h, info.mipLodBias); + fnvHashCombine(h, info.anisotropyEnable); + if(info.anisotropyEnable == VK_TRUE) + fnvHashCombine(h, info.maxAnisotropy); + fnvHashCombine(h, info.compareEnable); + if(info.compareEnable == VK_TRUE) + fnvHashCombine(h, info.compareOp); + fnvHashCombine(h, info.minLod); + fnvHashCombine(h, info.maxLod); + fnvHashCombine(h, info.borderColor); + fnvHashCombine(h, info.unnormalizedCoordinates); + // handle custom border color + VkBaseOutStructure* ext = (VkBaseOutStructure*)info.pNext; + while(ext) + { + if(ext->sType == VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT) + { + auto* extInfo = (VkSamplerCustomBorderColorCreateInfoEXT*)ext; + fnvHashCombine(h, extInfo->customBorderColor.uint32[0]); + fnvHashCombine(h, extInfo->customBorderColor.uint32[1]); + fnvHashCombine(h, extInfo->customBorderColor.uint32[2]); + fnvHashCombine(h, extInfo->customBorderColor.uint32[3]); + } + else + { + cemu_assert_unimplemented(); + } + ext = ext->pNext; + } + return h; +} + +std::unordered_map VKRObjectSampler::s_samplerCache; + +VKRObjectSampler::VKRObjectSampler(VkSamplerCreateInfo* samplerInfo) +{ + auto* vulkanRenderer = VulkanRenderer::GetInstance(); + if (vkCreateSampler(vulkanRenderer->GetLogicalDevice(), samplerInfo, nullptr, &m_sampler) != VK_SUCCESS) + vulkanRenderer->UnrecoverableError("Failed to create texture sampler"); + performanceMonitor.vk.numSamplers.increment(); + m_hash = CalcHashSamplerCreateInfo(*samplerInfo); +} + +VKRObjectSampler::~VKRObjectSampler() +{ + vkDestroySampler(VulkanRenderer::GetInstance()->GetLogicalDevice(), m_sampler, nullptr); + performanceMonitor.vk.numSamplers.decrement(); + // remove from cache + auto it = s_samplerCache.find(m_hash); + if(it != s_samplerCache.end()) + s_samplerCache.erase(it); +} + +void VKRObjectSampler::RefCountReachedZero() +{ + VulkanRenderer::GetInstance()->ReleaseDestructibleObject(this); +} + +VKRObjectSampler* VKRObjectSampler::GetOrCreateSampler(VkSamplerCreateInfo* samplerInfo) +{ + auto* vulkanRenderer = VulkanRenderer::GetInstance(); + uint64 hash = CalcHashSamplerCreateInfo(*samplerInfo); + auto it = s_samplerCache.find(hash); + if (it != s_samplerCache.end()) + { + auto* sampler = it->second; + return sampler; + } + auto* sampler = new VKRObjectSampler(samplerInfo); + s_samplerCache[hash] = sampler; + return sampler; +} + +void VKRObjectSampler::DestroyCache() +{ + // assuming all other objects which depend on vkSampler are destroyed, this cache should also have been emptied already + // but just to be sure lets still clear the cache + cemu_assert_debug(s_samplerCache.empty()); + for(auto& sampler : s_samplerCache) + { + cemu_assert_debug(sampler.second->m_refCount == 0); + delete sampler.second; + } + s_samplerCache.clear(); +} + VKRObjectRenderPass::VKRObjectRenderPass(AttachmentInfo_t& attachmentInfo, sint32 colorAttachmentCount) { // generate helper hash for pipeline state diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp index 3a684072..dd39bd88 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp @@ -727,7 +727,6 @@ VkDescriptorSetInfo* VulkanRenderer::draw_getOrCreateDescriptorSet(PipelineInfo* VkSamplerCustomBorderColorCreateInfoEXT samplerCustomBorderColor{}; - VkSampler sampler; VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; @@ -899,10 +898,9 @@ VkDescriptorSetInfo* VulkanRenderer::draw_getOrCreateDescriptorSet(PipelineInfo* } } } - - if (vkCreateSampler(m_logicalDevice, &samplerInfo, nullptr, &sampler) != VK_SUCCESS) - UnrecoverableError("Failed to create texture sampler"); - info.sampler = sampler; + VKRObjectSampler* samplerObj = VKRObjectSampler::GetOrCreateSampler(&samplerInfo); + vkObjDS->addRef(samplerObj); + info.sampler = samplerObj->GetSampler(); textureArray.emplace_back(info); } From 3738ccd2e676aa2c2cf2cdd5f08d5ac8dd221f57 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:55:23 +0100 Subject: [PATCH 058/137] Play bootSound.btsnd while shaders/pipelines are compiling (#1047) --- src/Cafe/HW/Latte/Core/LatteShaderCache.cpp | 122 +++++++++++++++++++- src/Cafe/OS/libs/snd_core/ax_out.cpp | 79 ++----------- src/audio/CubebAPI.cpp | 2 +- src/audio/DirectSoundAPI.cpp | 2 +- src/audio/IAudioAPI.cpp | 42 +++++++ src/audio/IAudioAPI.h | 9 +- src/audio/XAudio27API.cpp | 17 ++- src/audio/XAudio27API.h | 2 + src/audio/XAudio2API.cpp | 13 ++- src/audio/XAudio2API.h | 2 + src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 16 ++- src/gui/GeneralSettings2.cpp | 56 ++------- src/gui/GeneralSettings2.h | 1 + src/util/CMakeLists.txt | 2 + src/util/bootSound/BootSoundReader.cpp | 51 ++++++++ src/util/bootSound/BootSoundReader.h | 20 ++++ 17 files changed, 310 insertions(+), 128 deletions(-) create mode 100644 src/util/bootSound/BootSoundReader.cpp create mode 100644 src/util/bootSound/BootSoundReader.h diff --git a/src/Cafe/HW/Latte/Core/LatteShaderCache.cpp b/src/Cafe/HW/Latte/Core/LatteShaderCache.cpp index 9576eb2e..9b24de45 100644 --- a/src/Cafe/HW/Latte/Core/LatteShaderCache.cpp +++ b/src/Cafe/HW/Latte/Core/LatteShaderCache.cpp @@ -25,6 +25,9 @@ #include "util/helpers/Serializer.h" #include +#include