From 13b90874f9934f0a79a9ab2b9c4e1288ed2e6764 Mon Sep 17 00:00:00 2001 From: splatoon1enjoyer <131005903+splatoon1enjoyer@users.noreply.github.com> Date: Mon, 13 May 2024 14:52:25 +0000 Subject: [PATCH 01/73] Fix commas edge case in strings when parsing an assembly line (#1201) --- src/Cemu/PPCAssembler/ppcAssembler.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Cemu/PPCAssembler/ppcAssembler.cpp b/src/Cemu/PPCAssembler/ppcAssembler.cpp index 5bab7b8b..df20b21d 100644 --- a/src/Cemu/PPCAssembler/ppcAssembler.cpp +++ b/src/Cemu/PPCAssembler/ppcAssembler.cpp @@ -2418,6 +2418,9 @@ bool ppcAssembler_assembleSingleInstruction(char const* text, PPCAssemblerInOut* _ppcAssembler_translateAlias(instructionName); // parse operands internalInfo.listOperandStr.clear(); + + bool isInString = false; + while (currentPtr < endPtr) { currentPtr++; @@ -2425,7 +2428,10 @@ bool ppcAssembler_assembleSingleInstruction(char const* text, PPCAssemblerInOut* // find end of operand while (currentPtr < endPtr) { - if (*currentPtr == ',') + if (*currentPtr == '"') + isInString=!isInString; + + if (*currentPtr == ',' && !isInString) break; currentPtr++; } From 84e78088fb2d3d25797032fe963967aa2d1b5af0 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Sat, 4 May 2024 14:46:12 +0200 Subject: [PATCH 02/73] PPCCoreCallback: Add support for stack args if GPR limit is reached --- src/Cafe/HW/Espresso/PPCCallback.h | 37 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/Cafe/HW/Espresso/PPCCallback.h b/src/Cafe/HW/Espresso/PPCCallback.h index 19fcd4d1..3d5393b1 100644 --- a/src/Cafe/HW/Espresso/PPCCallback.h +++ b/src/Cafe/HW/Espresso/PPCCallback.h @@ -5,8 +5,28 @@ struct PPCCoreCallbackData_t { sint32 gprCount = 0; sint32 floatCount = 0; + sint32 stackCount = 0; }; +inline void _PPCCoreCallback_writeGPRArg(PPCCoreCallbackData_t& data, PPCInterpreter_t* hCPU, uint32 value) +{ + if (data.gprCount < 8) + { + hCPU->gpr[3 + data.gprCount] = value; + data.gprCount++; + } + else + { + uint32 stackOffset = 8 + data.stackCount * 4; + + // PPCCore_executeCallbackInternal does -16*4 to save the current stack area + stackOffset -= 16 * 4; + + memory_writeU32(hCPU->gpr[1] + stackOffset, value); + data.stackCount++; + } +} + // callback functions inline uint32 PPCCoreCallback(MPTR function, const PPCCoreCallbackData_t& data) { @@ -16,23 +36,21 @@ inline uint32 PPCCoreCallback(MPTR function, const PPCCoreCallbackData_t& data) template uint32 PPCCoreCallback(MPTR function, PPCCoreCallbackData_t& data, T currentArg, TArgs... args) { - cemu_assert_debug(data.gprCount <= 8); - cemu_assert_debug(data.floatCount <= 8); + // TODO float arguments on stack + cemu_assert_debug(data.floatCount < 8); + PPCInterpreter_t* hCPU = PPCInterpreter_getCurrentInstance(); if constexpr (std::is_pointer_v) { - hCPU->gpr[3 + data.gprCount] = MEMPTR(currentArg).GetMPTR(); - data.gprCount++; + _PPCCoreCallback_writeGPRArg(data, hCPU, MEMPTR(currentArg).GetMPTR()); } else if constexpr (std::is_base_of_v>) { - hCPU->gpr[3 + data.gprCount] = currentArg.GetMPTR(); - data.gprCount++; + _PPCCoreCallback_writeGPRArg(data, hCPU, currentArg.GetMPTR()); } else if constexpr (std::is_reference_v) { - hCPU->gpr[3 + data.gprCount] = MEMPTR(¤tArg).GetMPTR(); - data.gprCount++; + _PPCCoreCallback_writeGPRArg(data, hCPU, MEMPTR(¤tArg).GetMPTR()); } else if constexpr(std::is_enum_v) { @@ -53,8 +71,7 @@ uint32 PPCCoreCallback(MPTR function, PPCCoreCallbackData_t& data, T currentArg, } else { - hCPU->gpr[3 + data.gprCount] = (uint32)currentArg; - data.gprCount++; + _PPCCoreCallback_writeGPRArg(data, hCPU, (uint32)currentArg); } return PPCCoreCallback(function, data, args...); From 1c6b209692953bcf5a958499ba3ebba0e24d5c6f Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Sat, 4 May 2024 14:49:23 +0200 Subject: [PATCH 03/73] Add initial ntag and nfc implementation --- src/Cafe/CMakeLists.txt | 14 + src/Cafe/CafeSystem.cpp | 6 + src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp | 406 +++++++++++++++++ src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h | 31 ++ src/Cafe/OS/libs/nfc/TLV.cpp | 139 ++++++ src/Cafe/OS/libs/nfc/TLV.h | 37 ++ src/Cafe/OS/libs/nfc/TagV0.cpp | 301 +++++++++++++ src/Cafe/OS/libs/nfc/TagV0.h | 39 ++ src/Cafe/OS/libs/nfc/ndef.cpp | 277 ++++++++++++ src/Cafe/OS/libs/nfc/ndef.h | 88 ++++ src/Cafe/OS/libs/nfc/nfc.cpp | 596 +++++++++++++++++++++++++ src/Cafe/OS/libs/nfc/nfc.h | 62 +++ src/Cafe/OS/libs/nfc/stream.cpp | 201 +++++++++ src/Cafe/OS/libs/nfc/stream.h | 139 ++++++ src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp | 83 ++-- src/Cafe/OS/libs/nn_nfp/nn_nfp.h | 8 +- src/Cafe/OS/libs/ntag/ntag.cpp | 438 ++++++++++++++++++ src/Cafe/OS/libs/ntag/ntag.h | 94 ++++ src/Cemu/Logging/CemuLogging.cpp | 2 + src/Cemu/Logging/CemuLogging.h | 3 + src/gui/MainWindow.cpp | 10 +- 21 files changed, 2927 insertions(+), 47 deletions(-) create mode 100644 src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp create mode 100644 src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h create mode 100644 src/Cafe/OS/libs/nfc/TLV.cpp create mode 100644 src/Cafe/OS/libs/nfc/TLV.h create mode 100644 src/Cafe/OS/libs/nfc/TagV0.cpp create mode 100644 src/Cafe/OS/libs/nfc/TagV0.h create mode 100644 src/Cafe/OS/libs/nfc/ndef.cpp create mode 100644 src/Cafe/OS/libs/nfc/ndef.h create mode 100644 src/Cafe/OS/libs/nfc/nfc.cpp create mode 100644 src/Cafe/OS/libs/nfc/nfc.h create mode 100644 src/Cafe/OS/libs/nfc/stream.cpp create mode 100644 src/Cafe/OS/libs/nfc/stream.h create mode 100644 src/Cafe/OS/libs/ntag/ntag.cpp create mode 100644 src/Cafe/OS/libs/ntag/ntag.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 851854fc..b5090dcf 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -218,6 +218,8 @@ add_library(CemuCafe HW/SI/SI.cpp HW/SI/si.h HW/VI/VI.cpp + IOSU/ccr_nfc/iosu_ccr_nfc.cpp + IOSU/ccr_nfc/iosu_ccr_nfc.h IOSU/fsa/fsa_types.h IOSU/fsa/iosu_fsa.cpp IOSU/fsa/iosu_fsa.h @@ -378,6 +380,16 @@ add_library(CemuCafe OS/libs/h264_avc/parser/H264Parser.h OS/libs/mic/mic.cpp OS/libs/mic/mic.h + OS/libs/nfc/ndef.cpp + OS/libs/nfc/ndef.h + OS/libs/nfc/nfc.cpp + OS/libs/nfc/nfc.h + OS/libs/nfc/stream.cpp + OS/libs/nfc/stream.h + OS/libs/nfc/TagV0.cpp + OS/libs/nfc/TagV0.h + OS/libs/nfc/TLV.cpp + OS/libs/nfc/TLV.h OS/libs/nlibcurl/nlibcurl.cpp OS/libs/nlibcurl/nlibcurlDebug.hpp OS/libs/nlibcurl/nlibcurl.h @@ -453,6 +465,8 @@ add_library(CemuCafe OS/libs/nsyskbd/nsyskbd.h OS/libs/nsysnet/nsysnet.cpp OS/libs/nsysnet/nsysnet.h + OS/libs/ntag/ntag.cpp + OS/libs/ntag/ntag.h OS/libs/padscore/padscore.cpp OS/libs/padscore/padscore.h OS/libs/proc_ui/proc_ui.cpp diff --git a/src/Cafe/CafeSystem.cpp b/src/Cafe/CafeSystem.cpp index 3c62a686..958a5a57 100644 --- a/src/Cafe/CafeSystem.cpp +++ b/src/Cafe/CafeSystem.cpp @@ -35,6 +35,7 @@ #include "Cafe/IOSU/legacy/iosu_boss.h" #include "Cafe/IOSU/legacy/iosu_nim.h" #include "Cafe/IOSU/PDM/iosu_pdm.h" +#include "Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h" // IOSU initializer functions #include "Cafe/IOSU/kernel/iosu_kernel.h" @@ -51,6 +52,8 @@ #include "Cafe/OS/libs/gx2/GX2.h" #include "Cafe/OS/libs/gx2/GX2_Misc.h" #include "Cafe/OS/libs/mic/mic.h" +#include "Cafe/OS/libs/nfc/nfc.h" +#include "Cafe/OS/libs/ntag/ntag.h" #include "Cafe/OS/libs/nn_aoc/nn_aoc.h" #include "Cafe/OS/libs/nn_pdm/nn_pdm.h" #include "Cafe/OS/libs/nn_cmpt/nn_cmpt.h" @@ -533,6 +536,7 @@ namespace CafeSystem iosu::acp::GetModule(), iosu::fpd::GetModule(), iosu::pdm::GetModule(), + iosu::ccr_nfc::GetModule(), }; // initialize all subsystems which are persistent and don't depend on a game running @@ -587,6 +591,8 @@ namespace CafeSystem H264::Initialize(); snd_core::Initialize(); mic::Initialize(); + nfc::Initialize(); + ntag::Initialize(); // init hardware register interfaces HW_SI::Initialize(); } diff --git a/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp b/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp new file mode 100644 index 00000000..ff8ba2b1 --- /dev/null +++ b/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp @@ -0,0 +1,406 @@ +#include "iosu_ccr_nfc.h" +#include "Cafe/IOSU/kernel/iosu_kernel.h" +#include "util/crypto/aes128.h" +#include +#include + +namespace iosu +{ + namespace ccr_nfc + { + IOSMsgQueueId sCCRNFCMsgQueue; + SysAllocator sCCRNFCMsgQueueMsgBuffer; + std::thread sCCRNFCThread; + + constexpr uint8 sNfcKey[] = { 0xC1, 0x2B, 0x07, 0x10, 0xD7, 0x2C, 0xEB, 0x5D, 0x43, 0x49, 0xB7, 0x43, 0xE3, 0xCA, 0xD2, 0x24 }; + constexpr uint8 sNfcKeyIV[] = { 0x4F, 0xD3, 0x9A, 0x6E, 0x79, 0xFC, 0xEA, 0xAD, 0x99, 0x90, 0x4D, 0xB8, 0xEE, 0x38, 0xE9, 0xDB }; + + constexpr uint8 sUnfixedInfosMagicBytes[] = { 0x00, 0x00, 0xDB, 0x4B, 0x9E, 0x3F, 0x45, 0x27, 0x8F, 0x39, 0x7E, 0xFF, 0x9B, 0x4F, 0xB9, 0x93 }; + constexpr uint8 sLockedSecretMagicBytes[] = { 0xFD, 0xC8, 0xA0, 0x76, 0x94, 0xB8, 0x9E, 0x4C, 0x47, 0xD3, 0x7D, 0xE8, 0xCE, 0x5C, 0x74, 0xC1 }; + constexpr uint8 sUnfixedInfosString[] = { 0x75, 0x6E, 0x66, 0x69, 0x78, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x73, 0x00, 0x00, 0x00 }; + constexpr uint8 sLockedSecretString[] = { 0x6C, 0x6F, 0x63, 0x6B, 0x65, 0x64, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x00, 0x00, 0x00 }; + + constexpr uint8 sLockedSecretHmacKey[] = { 0x7F, 0x75, 0x2D, 0x28, 0x73, 0xA2, 0x00, 0x17, 0xFE, 0xF8, 0x5C, 0x05, 0x75, 0x90, 0x4B, 0x6D }; + constexpr uint8 sUnfixedInfosHmacKey[] = { 0x1D, 0x16, 0x4B, 0x37, 0x5B, 0x72, 0xA5, 0x57, 0x28, 0xB9, 0x1D, 0x64, 0xB6, 0xA3, 0xC2, 0x05 }; + + uint8 sLockedSecretInternalKey[0x10]; + uint8 sLockedSecretInternalNonce[0x10]; + uint8 sLockedSecretInternalHmacKey[0x10]; + + uint8 sUnfixedInfosInternalKey[0x10]; + uint8 sUnfixedInfosInternalNonce[0x10]; + uint8 sUnfixedInfosInternalHmacKey[0x10]; + + sint32 __CCRNFCValidateCryptData(CCRNFCCryptData* data, uint32 size, bool validateOffsets) + { + if (!data) + { + return CCR_NFC_ERROR; + } + + if (size != sizeof(CCRNFCCryptData)) + { + return CCR_NFC_ERROR; + } + + if (!validateOffsets) + { + return 0; + } + + // Make sure all offsets are within bounds + if (data->version == 0) + { + if (data->unfixedInfosHmacOffset < 0x1C9 && data->unfixedInfosOffset < 0x1C9 && + data->lockedSecretHmacOffset < 0x1C9 && data->lockedSecretOffset < 0x1C9 && + data->lockedSecretSize < 0x1C9 && data->unfixedInfosSize < 0x1C9) + { + return 0; + } + } + else if (data->version == 2) + { + if (data->unfixedInfosHmacOffset < 0x21D && data->unfixedInfosOffset < 0x21D && + data->lockedSecretHmacOffset < 0x21D && data->lockedSecretOffset < 0x21D && + data->lockedSecretSize < 0x21D && data->unfixedInfosSize < 0x21D) + { + return 0; + } + } + + return CCR_NFC_ERROR; + } + + sint32 CCRNFCAESCTRCrypt(const uint8* key, const void* ivNonce, const void* inData, uint32_t inSize, void* outData, uint32_t outSize) + { + uint8_t tmpIv[0x10]; + memcpy(tmpIv, ivNonce, sizeof(tmpIv)); + + memcpy(outData, inData, inSize); + AES128CTR_transform((uint8*)outData, outSize, (uint8*)key, tmpIv); + return 0; + } + + sint32 __CCRNFCGenerateKey(const uint8* hmacKey, uint32 hmacKeySize, const uint8* name, uint32_t nameSize, const uint8* inData, uint32_t inSize, uint8* outData, uint32_t outSize) + { + if (nameSize != 0xe || outSize != 0x40) + { + return CCR_NFC_ERROR; + } + + // Create a buffer containing 2 counter bytes, the key name, and the key data + uint8_t buffer[0x50]; + buffer[0] = 0; + buffer[1] = 0; + memcpy(buffer + 2, name, nameSize); + memcpy(buffer + nameSize + 2, inData, inSize); + + uint16_t counter = 0; + while (outSize > 0) + { + // Set counter bytes and increment counter + buffer[0] = (counter >> 8) & 0xFF; + buffer[1] = counter & 0xFF; + counter++; + + uint32 dataSize = outSize; + if (!HMAC(EVP_sha256(), hmacKey, hmacKeySize, buffer, sizeof(buffer), outData, &dataSize)) + { + return CCR_NFC_ERROR; + } + + outSize -= 0x20; + outData += 0x20; + } + + return 0; + } + + sint32 __CCRNFCGenerateInternalKeys(const CCRNFCCryptData* in, const uint8* keyGenSalt) + { + uint8_t lockedSecretBuffer[0x40] = { 0 }; + uint8_t unfixedInfosBuffer[0x40] = { 0 }; + uint8_t outBuffer[0x40] = { 0 }; + + // Fill the locked secret buffer + memcpy(lockedSecretBuffer, sLockedSecretMagicBytes, sizeof(sLockedSecretMagicBytes)); + if (in->version == 0) + { + // For Version 0 this is the 16-byte Format Info + memcpy(lockedSecretBuffer + 0x10, in->data + in->uuidOffset, 0x10); + } + else if (in->version == 2) + { + // For Version 2 this is 2 times the 7-byte UID + 1 check byte + memcpy(lockedSecretBuffer + 0x10, in->data + in->uuidOffset, 8); + memcpy(lockedSecretBuffer + 0x18, in->data + in->uuidOffset, 8); + } + else + { + return CCR_NFC_ERROR; + } + // Append key generation salt + memcpy(lockedSecretBuffer + 0x20, keyGenSalt, 0x20); + + // Generate the key output + sint32 res = __CCRNFCGenerateKey(sLockedSecretHmacKey, sizeof(sLockedSecretHmacKey), sLockedSecretString, 0xe, lockedSecretBuffer, sizeof(lockedSecretBuffer), outBuffer, sizeof(outBuffer)); + if (res != 0) + { + return res; + } + + // Unpack the key buffer + memcpy(sLockedSecretInternalKey, outBuffer, 0x10); + memcpy(sLockedSecretInternalNonce, outBuffer + 0x10, 0x10); + memcpy(sLockedSecretInternalHmacKey, outBuffer + 0x20, 0x10); + + // Fill the unfixed infos buffer + memcpy(unfixedInfosBuffer, in->data + in->seedOffset, 2); + memcpy(unfixedInfosBuffer + 2, sUnfixedInfosMagicBytes + 2, 0xe); + if (in->version == 0) + { + // For Version 0 this is the 16-byte Format Info + memcpy(unfixedInfosBuffer + 0x10, in->data + in->uuidOffset, 0x10); + } + else if (in->version == 2) + { + // For Version 2 this is 2 times the 7-byte UID + 1 check byte + memcpy(unfixedInfosBuffer + 0x10, in->data + in->uuidOffset, 8); + memcpy(unfixedInfosBuffer + 0x18, in->data + in->uuidOffset, 8); + } + else + { + return CCR_NFC_ERROR; + } + // Append key generation salt + memcpy(unfixedInfosBuffer + 0x20, keyGenSalt, 0x20); + + // Generate the key output + res = __CCRNFCGenerateKey(sUnfixedInfosHmacKey, sizeof(sUnfixedInfosHmacKey), sUnfixedInfosString, 0xe, unfixedInfosBuffer, sizeof(unfixedInfosBuffer), outBuffer, sizeof(outBuffer)); + if (res != 0) + { + return res; + } + + // Unpack the key buffer + memcpy(sUnfixedInfosInternalKey, outBuffer, 0x10); + memcpy(sUnfixedInfosInternalNonce, outBuffer + 0x10, 0x10); + memcpy(sUnfixedInfosInternalHmacKey, outBuffer + 0x20, 0x10); + + return 0; + } + + sint32 __CCRNFCCryptData(const CCRNFCCryptData* in, CCRNFCCryptData* out, bool decrypt) + { + // Decrypt key generation salt + uint8_t keyGenSalt[0x20]; + sint32 res = CCRNFCAESCTRCrypt(sNfcKey, sNfcKeyIV, in->data + in->keyGenSaltOffset, 0x20, keyGenSalt, sizeof(keyGenSalt)); + if (res != 0) + { + return res; + } + + // Prepare internal keys + res = __CCRNFCGenerateInternalKeys(in, keyGenSalt); + if (res != 0) + { + return res; + } + + if (decrypt) + { + // Only version 0 tags have an encrypted locked secret area + if (in->version == 0) + { + res = CCRNFCAESCTRCrypt(sLockedSecretInternalKey, sLockedSecretInternalNonce, in->data + in->lockedSecretOffset, in->lockedSecretSize, out->data + in->lockedSecretOffset, in->lockedSecretSize); + if (res != 0) + { + return res; + } + } + + // Decrypt unfxied infos + res = CCRNFCAESCTRCrypt(sUnfixedInfosInternalKey, sUnfixedInfosInternalNonce, in->data + in->unfixedInfosOffset, in->unfixedInfosSize, out->data + in->unfixedInfosOffset, in->unfixedInfosSize); + if (res != 0) + { + return res; + } + + // Verify HMACs + uint8_t hmacBuffer[0x20]; + uint32 hmacLen = sizeof(hmacBuffer); + + if (!HMAC(EVP_sha256(), sLockedSecretInternalHmacKey, sizeof(sLockedSecretInternalHmacKey), out->data + in->lockedSecretHmacOffset + 0x20, (in->dataSize - in->lockedSecretHmacOffset) - 0x20, hmacBuffer, &hmacLen)) + { + return CCR_NFC_ERROR; + } + + if (memcmp(in->data + in->lockedSecretHmacOffset, hmacBuffer, 0x20) != 0) + { + return CCR_NFC_INVALID_LOCKED_SECRET; + } + + if (in->version == 0) + { + hmacLen = sizeof(hmacBuffer); + res = HMAC(EVP_sha256(), sUnfixedInfosInternalHmacKey, sizeof(sUnfixedInfosInternalHmacKey), out->data + in->unfixedInfosHmacOffset + 0x20, (in->dataSize - in->unfixedInfosHmacOffset) - 0x20, hmacBuffer, &hmacLen) ? 0 : CCR_NFC_ERROR; + } + else + { + hmacLen = sizeof(hmacBuffer); + res = HMAC(EVP_sha256(), sUnfixedInfosInternalHmacKey, sizeof(sUnfixedInfosInternalHmacKey), out->data + in->unfixedInfosHmacOffset + 0x21, (in->dataSize - in->unfixedInfosHmacOffset) - 0x21, hmacBuffer, &hmacLen) ? 0 : CCR_NFC_ERROR; + } + + if (memcmp(in->data + in->unfixedInfosHmacOffset, hmacBuffer, 0x20) != 0) + { + return CCR_NFC_INVALID_UNFIXED_INFOS; + } + } + else + { + uint8_t hmacBuffer[0x20]; + uint32 hmacLen = sizeof(hmacBuffer); + + if (!HMAC(EVP_sha256(), sLockedSecretInternalHmacKey, sizeof(sLockedSecretInternalHmacKey), out->data + in->lockedSecretHmacOffset + 0x20, (in->dataSize - in->lockedSecretHmacOffset) - 0x20, hmacBuffer, &hmacLen)) + { + return CCR_NFC_ERROR; + } + + if (memcmp(in->data + in->lockedSecretHmacOffset, hmacBuffer, 0x20) != 0) + { + return CCR_NFC_INVALID_LOCKED_SECRET; + } + + // Only version 0 tags have an encrypted locked secret area + if (in->version == 0) + { + uint32 hmacLen = 0x20; + if (!HMAC(EVP_sha256(), sUnfixedInfosInternalHmacKey, sizeof(sUnfixedInfosInternalHmacKey), out->data + in->unfixedInfosHmacOffset + 0x20, (in->dataSize - in->unfixedInfosHmacOffset) - 0x20, out->data + in->unfixedInfosHmacOffset, &hmacLen)) + { + return CCR_NFC_ERROR; + } + + res = CCRNFCAESCTRCrypt(sLockedSecretInternalKey, sLockedSecretInternalNonce, in->data + in->lockedSecretOffset, in->lockedSecretSize, out->data + in->lockedSecretOffset, in->lockedSecretSize); + if (res != 0) + { + return res; + } + } + else + { + uint32 hmacLen = 0x20; + if (!HMAC(EVP_sha256(), sUnfixedInfosInternalHmacKey, sizeof(sUnfixedInfosInternalHmacKey), out->data + in->unfixedInfosHmacOffset + 0x21, (in->dataSize - in->unfixedInfosHmacOffset) - 0x21, out->data + in->unfixedInfosHmacOffset, &hmacLen)) + { + return CCR_NFC_ERROR; + } + } + + res = CCRNFCAESCTRCrypt(sUnfixedInfosInternalKey, sUnfixedInfosInternalNonce, in->data + in->unfixedInfosOffset, in->unfixedInfosSize, out->data + in->unfixedInfosOffset, in->unfixedInfosSize); + if (res != 0) + { + return res; + } + } + + return res; + } + + void CCRNFCThread() + { + iosu::kernel::IOSMessage msg; + while (true) + { + IOS_ERROR error = iosu::kernel::IOS_ReceiveMessage(sCCRNFCMsgQueue, &msg, 0); + cemu_assert(!IOS_ResultIsError(error)); + + // Check for system exit + if (msg == 0xf00dd00d) + { + return; + } + + IPCCommandBody* cmd = MEMPTR(msg).GetPtr(); + if (cmd->cmdId == IPCCommandId::IOS_OPEN) + { + iosu::kernel::IOS_ResourceReply(cmd, IOS_ERROR_OK); + } + else if (cmd->cmdId == IPCCommandId::IOS_CLOSE) + { + iosu::kernel::IOS_ResourceReply(cmd, IOS_ERROR_OK); + } + else if (cmd->cmdId == IPCCommandId::IOS_IOCTL) + { + sint32 result; + uint32 requestId = cmd->args[0]; + void* ptrIn = MEMPTR(cmd->args[1]); + uint32 sizeIn = cmd->args[2]; + void* ptrOut = MEMPTR(cmd->args[3]); + uint32 sizeOut = cmd->args[4]; + + if ((result = __CCRNFCValidateCryptData(static_cast(ptrIn), sizeIn, true)) == 0 && + (result = __CCRNFCValidateCryptData(static_cast(ptrOut), sizeOut, false)) == 0) + { + // Initialize outData with inData + memcpy(ptrOut, ptrIn, sizeIn); + + switch (requestId) + { + case 1: // encrypt + result = __CCRNFCCryptData(static_cast(ptrIn), static_cast(ptrOut), false); + break; + case 2: // decrypt + result = __CCRNFCCryptData(static_cast(ptrIn), static_cast(ptrOut), true); + break; + default: + cemuLog_log(LogType::Force, "/dev/ccr_nfc: Unsupported IOCTL requestId"); + cemu_assert_suspicious(); + result = IOS_ERROR_INVALID; + break; + } + } + + iosu::kernel::IOS_ResourceReply(cmd, static_cast(result)); + } + else + { + cemuLog_log(LogType::Force, "/dev/ccr_nfc: Unsupported IPC cmdId"); + cemu_assert_suspicious(); + iosu::kernel::IOS_ResourceReply(cmd, IOS_ERROR_INVALID); + } + } + } + + class : public ::IOSUModule + { + void SystemLaunch() override + { + sCCRNFCMsgQueue = iosu::kernel::IOS_CreateMessageQueue(sCCRNFCMsgQueueMsgBuffer.GetPtr(), sCCRNFCMsgQueueMsgBuffer.GetCount()); + cemu_assert(!IOS_ResultIsError(static_cast(sCCRNFCMsgQueue))); + + IOS_ERROR error = iosu::kernel::IOS_RegisterResourceManager("/dev/ccr_nfc", sCCRNFCMsgQueue); + cemu_assert(!IOS_ResultIsError(error)); + + sCCRNFCThread = std::thread(CCRNFCThread); + } + + void SystemExit() override + { + if (sCCRNFCMsgQueue < 0) + { + return; + } + + iosu::kernel::IOS_SendMessage(sCCRNFCMsgQueue, 0xf00dd00d, 0); + sCCRNFCThread.join(); + + iosu::kernel::IOS_DestroyMessageQueue(sCCRNFCMsgQueue); + sCCRNFCMsgQueue = -1; + } + } sIOSUModuleCCRNFC; + + IOSUModule* GetModule() + { + return &sIOSUModuleCCRNFC; + } + } +} diff --git a/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h b/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h new file mode 100644 index 00000000..ae99d645 --- /dev/null +++ b/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h @@ -0,0 +1,31 @@ +#pragma once +#include "Cafe/IOSU/iosu_types_common.h" + +#define CCR_NFC_ERROR (-0x2F001E) +#define CCR_NFC_INVALID_LOCKED_SECRET (-0x2F0029) +#define CCR_NFC_INVALID_UNFIXED_INFOS (-0x2F002A) + +namespace iosu +{ + namespace ccr_nfc + { + struct CCRNFCCryptData + { + uint32 version; + uint32 dataSize; + uint32 seedOffset; + uint32 keyGenSaltOffset; + uint32 uuidOffset; + uint32 unfixedInfosOffset; + uint32 unfixedInfosSize; + uint32 lockedSecretOffset; + uint32 lockedSecretSize; + uint32 unfixedInfosHmacOffset; + uint32 lockedSecretHmacOffset; + uint8 data[540]; + }; + static_assert(sizeof(CCRNFCCryptData) == 0x248); + + IOSUModule* GetModule(); + } +} diff --git a/src/Cafe/OS/libs/nfc/TLV.cpp b/src/Cafe/OS/libs/nfc/TLV.cpp new file mode 100644 index 00000000..99536428 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/TLV.cpp @@ -0,0 +1,139 @@ +#include "TLV.h" +#include "stream.h" + +#include + +TLV::TLV() +{ +} + +TLV::TLV(Tag tag, std::vector value) + : mTag(tag), mValue(std::move(value)) +{ +} + +TLV::~TLV() +{ +} + +std::vector TLV::FromBytes(const std::span& data) +{ + bool hasTerminator = false; + std::vector tlvs; + SpanStream stream(data, std::endian::big); + + while (stream.GetRemaining() > 0 && !hasTerminator) + { + // Read the tag + uint8_t byte; + stream >> byte; + Tag tag = static_cast(byte); + + switch (tag) + { + case TLV::TAG_NULL: + // Don't need to do anything for NULL tags + break; + + case TLV::TAG_TERMINATOR: + tlvs.emplace_back(tag, std::vector{}); + hasTerminator = true; + break; + + default: + { + // Read the length + uint16_t length; + stream >> byte; + length = byte; + + // If the length is 0xff, 2 bytes with length follow + if (length == 0xff) { + stream >> length; + } + + std::vector value; + value.resize(length); + stream.Read(value); + + tlvs.emplace_back(tag, value); + break; + } + } + + if (stream.GetError() != Stream::ERROR_OK) + { + cemuLog_log(LogType::Force, "Error: TLV parsing read past end of stream"); + // Clear tlvs to prevent further havoc while parsing ndef data + tlvs.clear(); + break; + } + } + + // This seems to be okay, at least NTAGs don't add a terminator tag + // if (!hasTerminator) + // { + // cemuLog_log(LogType::Force, "Warning: TLV parsing reached end of stream without terminator tag"); + // } + + return tlvs; +} + +std::vector TLV::ToBytes() const +{ + std::vector bytes; + VectorStream stream(bytes, std::endian::big); + + // Write tag + stream << std::uint8_t(mTag); + + switch (mTag) + { + case TLV::TAG_NULL: + case TLV::TAG_TERMINATOR: + // Nothing to do here + break; + + default: + { + // Write length (decide if as a 8-bit or 16-bit value) + if (mValue.size() >= 0xff) + { + stream << std::uint8_t(0xff); + stream << std::uint16_t(mValue.size()); + } + else + { + stream << std::uint8_t(mValue.size()); + } + + // Write value + stream.Write(mValue); + } + } + + return bytes; +} + +TLV::Tag TLV::GetTag() const +{ + return mTag; +} + +const std::vector& TLV::GetValue() const +{ + return mValue; +} + +void TLV::SetTag(Tag tag) +{ + mTag = tag; +} + +void TLV::SetValue(const std::span& value) +{ + // Can only write max 16-bit lengths into TLV + cemu_assert(value.size() < 0x10000); + + mValue.assign(value.begin(), value.end()); +} diff --git a/src/Cafe/OS/libs/nfc/TLV.h b/src/Cafe/OS/libs/nfc/TLV.h new file mode 100644 index 00000000..f582128f --- /dev/null +++ b/src/Cafe/OS/libs/nfc/TLV.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +class TLV +{ +public: + enum Tag + { + TAG_NULL = 0x00, + TAG_LOCK_CTRL = 0x01, + TAG_MEM_CTRL = 0x02, + TAG_NDEF = 0x03, + TAG_PROPRIETARY = 0xFD, + TAG_TERMINATOR = 0xFE, + }; + +public: + TLV(); + TLV(Tag tag, std::vector value); + virtual ~TLV(); + + static std::vector FromBytes(const std::span& data); + std::vector ToBytes() const; + + Tag GetTag() const; + const std::vector& GetValue() const; + + void SetTag(Tag tag); + void SetValue(const std::span& value); + +private: + Tag mTag; + std::vector mValue; +}; diff --git a/src/Cafe/OS/libs/nfc/TagV0.cpp b/src/Cafe/OS/libs/nfc/TagV0.cpp new file mode 100644 index 00000000..8b5a8143 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/TagV0.cpp @@ -0,0 +1,301 @@ +#include "TagV0.h" +#include "TLV.h" + +#include + +namespace +{ + +constexpr std::size_t kTagSize = 512u; +constexpr std::size_t kMaxBlockCount = kTagSize / sizeof(TagV0::Block); + +constexpr std::uint8_t kLockbyteBlock0 = 0xe; +constexpr std::uint8_t kLockbytesStart0 = 0x0; +constexpr std::uint8_t kLockbytesEnd0 = 0x2; +constexpr std::uint8_t kLockbyteBlock1 = 0xf; +constexpr std::uint8_t kLockbytesStart1 = 0x2; +constexpr std::uint8_t kLockbytesEnd1 = 0x8; + +constexpr std::uint8_t kNDEFMagicNumber = 0xe1; + +// These blocks are not part of the locked area +constexpr bool IsBlockLockedOrReserved(std::uint8_t blockIdx) +{ + // Block 0 is the UID + if (blockIdx == 0x0) + { + return true; + } + + // Block 0xd is reserved + if (blockIdx == 0xd) + { + return true; + } + + // Block 0xe and 0xf contains lock / reserved bytes + if (blockIdx == 0xe || blockIdx == 0xf) + { + return true; + } + + return false; +} + +} // namespace + +TagV0::TagV0() +{ +} + +TagV0::~TagV0() +{ +} + +std::shared_ptr TagV0::FromBytes(const std::span& data) +{ + // Version 0 tags need at least 512 bytes + if (data.size() != kTagSize) + { + cemuLog_log(LogType::Force, "Error: Version 0 tags should be {} bytes in size", kTagSize); + return {}; + } + + std::shared_ptr tag = std::make_shared(); + + // Parse the locked area before continuing + if (!tag->ParseLockedArea(data)) + { + cemuLog_log(LogType::Force, "Error: Failed to parse locked area"); + return {}; + } + + // Now that the locked area is known, parse the data area + std::vector dataArea; + if (!tag->ParseDataArea(data, dataArea)) + { + cemuLog_log(LogType::Force, "Error: Failed to parse data area"); + return {}; + } + + // The first few bytes in the dataArea make up the capability container + std::copy_n(dataArea.begin(), tag->mCapabilityContainer.size(), std::as_writable_bytes(std::span(tag->mCapabilityContainer)).begin()); + if (!tag->ValidateCapabilityContainer()) + { + cemuLog_log(LogType::Force, "Error: Failed to validate capability container"); + return {}; + } + + // The rest of the dataArea contains the TLVs + tag->mTLVs = TLV::FromBytes(std::span(dataArea).subspan(tag->mCapabilityContainer.size())); + if (tag->mTLVs.empty()) + { + cemuLog_log(LogType::Force, "Error: Tag contains no TLVs"); + return {}; + } + + // Look for the NDEF tlv + tag->mNdefTlvIdx = static_cast(-1); + for (std::size_t i = 0; i < tag->mTLVs.size(); i++) + { + if (tag->mTLVs[i].GetTag() == TLV::TAG_NDEF) + { + tag->mNdefTlvIdx = i; + break; + } + } + + if (tag->mNdefTlvIdx == static_cast(-1)) + { + cemuLog_log(LogType::Force, "Error: Tag contains no NDEF TLV"); + return {}; + } + + // Append locked data + for (const auto& [key, value] : tag->mLockedBlocks) + { + tag->mLockedArea.insert(tag->mLockedArea.end(), value.begin(), value.end()); + } + + return tag; +} + +std::vector TagV0::ToBytes() const +{ + std::vector bytes(kTagSize); + + // Insert locked or reserved blocks + for (const auto& [key, value] : mLockedOrReservedBlocks) + { + std::copy(value.begin(), value.end(), bytes.begin() + key * sizeof(Block)); + } + + // Insert locked area + auto lockedDataIterator = mLockedArea.begin(); + for (const auto& [key, value] : mLockedBlocks) + { + std::copy_n(lockedDataIterator, sizeof(Block), bytes.begin() + key * sizeof(Block)); + lockedDataIterator += sizeof(Block); + } + + // Pack the dataArea into a linear buffer + std::vector dataArea; + const auto ccBytes = std::as_bytes(std::span(mCapabilityContainer)); + dataArea.insert(dataArea.end(), ccBytes.begin(), ccBytes.end()); + for (const TLV& tlv : mTLVs) + { + const auto tlvBytes = tlv.ToBytes(); + dataArea.insert(dataArea.end(), tlvBytes.begin(), tlvBytes.end()); + } + + // Make sure the dataArea is block size aligned + dataArea.resize((dataArea.size() + (sizeof(Block)-1)) & ~(sizeof(Block)-1)); + + // The rest will be the data area + auto dataIterator = dataArea.begin(); + for (std::uint8_t currentBlock = 0; currentBlock < kMaxBlockCount; currentBlock++) + { + // All blocks which aren't locked make up the dataArea + if (!IsBlockLocked(currentBlock)) + { + std::copy_n(dataIterator, sizeof(Block), bytes.begin() + currentBlock * sizeof(Block)); + dataIterator += sizeof(Block); + } + } + + return bytes; +} + +const TagV0::Block& TagV0::GetUIDBlock() const +{ + return mLockedOrReservedBlocks.at(0); +} + +const std::vector& TagV0::GetNDEFData() const +{ + return mTLVs[mNdefTlvIdx].GetValue(); +} + +const std::vector& TagV0::GetLockedArea() const +{ + return mLockedArea; +} + +void TagV0::SetNDEFData(const std::span& data) +{ + // Update the ndef value + mTLVs[mNdefTlvIdx].SetValue(data); +} + +bool TagV0::ParseLockedArea(const std::span& data) +{ + std::uint8_t currentBlock = 0; + + // Start by parsing the first set of lock bytes + for (std::uint8_t i = kLockbytesStart0; i < kLockbytesEnd0; i++) + { + std::uint8_t lockByte = std::uint8_t(data[kLockbyteBlock0 * sizeof(Block) + i]); + + // Iterate over the individual bits in the lock byte + for (std::uint8_t j = 0; j < 8; j++) + { + // Is block locked? + if (lockByte & (1u << j)) + { + Block blk; + std::copy_n(data.begin() + currentBlock * sizeof(Block), sizeof(Block), blk.begin()); + + // The lock bytes themselves are not part of the locked area + if (!IsBlockLockedOrReserved(currentBlock)) + { + mLockedBlocks.emplace(currentBlock, blk); + } + else + { + mLockedOrReservedBlocks.emplace(currentBlock, blk); + } + } + + currentBlock++; + } + } + + // Parse the second set of lock bytes + for (std::uint8_t i = kLockbytesStart1; i < kLockbytesEnd1; i++) { + std::uint8_t lockByte = std::uint8_t(data[kLockbyteBlock1 * sizeof(Block) + i]); + + // Iterate over the individual bits in the lock byte + for (std::uint8_t j = 0; j < 8; j++) + { + // Is block locked? + if (lockByte & (1u << j)) + { + Block blk; + std::copy_n(data.begin() + currentBlock * sizeof(Block), sizeof(Block), blk.begin()); + + // The lock bytes themselves are not part of the locked area + if (!IsBlockLockedOrReserved(currentBlock)) + { + mLockedBlocks.emplace(currentBlock, blk); + } + else + { + mLockedOrReservedBlocks.emplace(currentBlock, blk); + } + } + + currentBlock++; + } + } + + return true; +} + +bool TagV0::IsBlockLocked(std::uint8_t blockIdx) const +{ + return mLockedBlocks.contains(blockIdx) || IsBlockLockedOrReserved(blockIdx); +} + +bool TagV0::ParseDataArea(const std::span& data, std::vector& dataArea) +{ + for (std::uint8_t currentBlock = 0; currentBlock < kMaxBlockCount; currentBlock++) + { + // All blocks which aren't locked make up the dataArea + if (!IsBlockLocked(currentBlock)) + { + auto blockOffset = data.begin() + sizeof(Block) * currentBlock; + dataArea.insert(dataArea.end(), blockOffset, blockOffset + sizeof(Block)); + } + } + + return true; +} + +bool TagV0::ValidateCapabilityContainer() +{ + // NDEF Magic Number + std::uint8_t nmn = mCapabilityContainer[0]; + if (nmn != kNDEFMagicNumber) + { + cemuLog_log(LogType::Force, "Error: CC: Invalid NDEF Magic Number"); + return false; + } + + // Version Number + std::uint8_t vno = mCapabilityContainer[1]; + if (vno >> 4 != 1) + { + cemuLog_log(LogType::Force, "Error: CC: Invalid Version Number"); + return false; + } + + // Tag memory size + std::uint8_t tms = mCapabilityContainer[2]; + if (8u * (tms + 1) < kTagSize) + { + cemuLog_log(LogType::Force, "Error: CC: Incomplete tag memory size"); + return false; + } + + return true; +} diff --git a/src/Cafe/OS/libs/nfc/TagV0.h b/src/Cafe/OS/libs/nfc/TagV0.h new file mode 100644 index 00000000..1d0e88d7 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/TagV0.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include "TLV.h" + +class TagV0 +{ +public: + using Block = std::array; + +public: + TagV0(); + virtual ~TagV0(); + + static std::shared_ptr FromBytes(const std::span& data); + std::vector ToBytes() const; + + const Block& GetUIDBlock() const; + const std::vector& GetNDEFData() const; + const std::vector& GetLockedArea() const; + + void SetNDEFData(const std::span& data); + +private: + bool ParseLockedArea(const std::span& data); + bool IsBlockLocked(std::uint8_t blockIdx) const; + bool ParseDataArea(const std::span& data, std::vector& dataArea); + bool ValidateCapabilityContainer(); + + std::map mLockedOrReservedBlocks; + std::map mLockedBlocks; + std::array mCapabilityContainer; + std::vector mTLVs; + std::size_t mNdefTlvIdx; + std::vector mLockedArea; +}; diff --git a/src/Cafe/OS/libs/nfc/ndef.cpp b/src/Cafe/OS/libs/nfc/ndef.cpp new file mode 100644 index 00000000..f8d87fb8 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/ndef.cpp @@ -0,0 +1,277 @@ +#include "ndef.h" + +#include + +namespace ndef +{ + + Record::Record() + { + } + + Record::~Record() + { + } + + std::optional Record::FromStream(Stream& stream) + { + Record rec; + + // Read record header + uint8_t recHdr; + stream >> recHdr; + rec.mFlags = recHdr & ~NDEF_TNF_MASK; + rec.mTNF = static_cast(recHdr & NDEF_TNF_MASK); + + // Type length + uint8_t typeLen; + stream >> typeLen; + + // Payload length; + uint32_t payloadLen; + if (recHdr & NDEF_SR) + { + uint8_t len; + stream >> len; + payloadLen = len; + } + else + { + stream >> payloadLen; + } + + // Some sane limits for the payload size + if (payloadLen > 2 * 1024 * 1024) + { + return {}; + } + + // ID length + uint8_t idLen = 0; + if (recHdr & NDEF_IL) + { + stream >> idLen; + } + + // Make sure we didn't read past the end of the stream yet + if (stream.GetError() != Stream::ERROR_OK) + { + return {}; + } + + // Type + rec.mType.resize(typeLen); + stream.Read(rec.mType); + + // ID + rec.mID.resize(idLen); + stream.Read(rec.mID); + + // Payload + rec.mPayload.resize(payloadLen); + stream.Read(rec.mPayload); + + // Make sure we didn't read past the end of the stream again + if (stream.GetError() != Stream::ERROR_OK) + { + return {}; + } + + return rec; + } + + std::vector Record::ToBytes(uint8_t flags) const + { + std::vector bytes; + VectorStream stream(bytes, std::endian::big); + + // Combine flags (clear message begin and end flags) + std::uint8_t finalFlags = mFlags & ~(NDEF_MB | NDEF_ME); + finalFlags |= flags; + + // Write flags + tnf + stream << std::uint8_t(finalFlags | std::uint8_t(mTNF)); + + // Type length + stream << std::uint8_t(mType.size()); + + // Payload length + if (IsShort()) + { + stream << std::uint8_t(mPayload.size()); + } + else + { + stream << std::uint32_t(mPayload.size()); + } + + // ID length + if (mFlags & NDEF_IL) + { + stream << std::uint8_t(mID.size()); + } + + // Type + stream.Write(mType); + + // ID + stream.Write(mID); + + // Payload + stream.Write(mPayload); + + return bytes; + } + + Record::TypeNameFormat Record::GetTNF() const + { + return mTNF; + } + + const std::vector& Record::GetID() const + { + return mID; + } + + const std::vector& Record::GetType() const + { + return mType; + } + + const std::vector& Record::GetPayload() const + { + return mPayload; + } + + void Record::SetTNF(TypeNameFormat tnf) + { + mTNF = tnf; + } + + void Record::SetID(const std::span& id) + { + cemu_assert(id.size() < 0x100); + + if (id.size() > 0) + { + mFlags |= NDEF_IL; + } + else + { + mFlags &= ~NDEF_IL; + } + + mID.assign(id.begin(), id.end()); + } + + void Record::SetType(const std::span& type) + { + cemu_assert(type.size() < 0x100); + + mType.assign(type.begin(), type.end()); + } + + void Record::SetPayload(const std::span& payload) + { + // Update short record flag + if (payload.size() < 0xff) + { + mFlags |= NDEF_SR; + } + else + { + mFlags &= ~NDEF_SR; + } + + mPayload.assign(payload.begin(), payload.end()); + } + + bool Record::IsLast() const + { + return mFlags & NDEF_ME; + } + + bool Record::IsShort() const + { + return mFlags & NDEF_SR; + } + + Message::Message() + { + } + + Message::~Message() + { + } + + std::optional Message::FromBytes(const std::span& data) + { + Message msg; + SpanStream stream(data, std::endian::big); + + while (stream.GetRemaining() > 0) + { + std::optional rec = Record::FromStream(stream); + if (!rec) + { + cemuLog_log(LogType::Force, "Warning: Failed to parse NDEF Record #{}." + "Ignoring the remaining {} bytes in NDEF message", msg.mRecords.size(), stream.GetRemaining()); + break; + } + + msg.mRecords.emplace_back(*rec); + + if ((*rec).IsLast() && stream.GetRemaining() > 0) + { + cemuLog_log(LogType::Force, "Warning: Ignoring {} bytes in NDEF message", stream.GetRemaining()); + break; + } + } + + if (msg.mRecords.empty()) + { + return {}; + } + + if (!msg.mRecords.back().IsLast()) + { + cemuLog_log(LogType::Force, "Error: NDEF message missing end record"); + return {}; + } + + return msg; + } + + std::vector Message::ToBytes() const + { + std::vector bytes; + + for (std::size_t i = 0; i < mRecords.size(); i++) + { + std::uint8_t flags = 0; + + // Add message begin flag to first record + if (i == 0) + { + flags |= Record::NDEF_MB; + } + + // Add message end flag to last record + if (i == mRecords.size() - 1) + { + flags |= Record::NDEF_ME; + } + + std::vector recordBytes = mRecords[i].ToBytes(flags); + bytes.insert(bytes.end(), recordBytes.begin(), recordBytes.end()); + } + + return bytes; + } + + void Message::append(const Record& r) + { + mRecords.push_back(r); + } + +} // namespace ndef diff --git a/src/Cafe/OS/libs/nfc/ndef.h b/src/Cafe/OS/libs/nfc/ndef.h new file mode 100644 index 00000000..b5f38b17 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/ndef.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +#include "stream.h" + +namespace ndef +{ + + class Record + { + public: + enum HeaderFlag + { + NDEF_IL = 0x08, + NDEF_SR = 0x10, + NDEF_CF = 0x20, + NDEF_ME = 0x40, + NDEF_MB = 0x80, + NDEF_TNF_MASK = 0x07, + }; + + enum TypeNameFormat + { + NDEF_TNF_EMPTY = 0, + NDEF_TNF_WKT = 1, + NDEF_TNF_MEDIA = 2, + NDEF_TNF_URI = 3, + NDEF_TNF_EXT = 4, + NDEF_TNF_UNKNOWN = 5, + NDEF_TNF_UNCHANGED = 6, + NDEF_TNF_RESERVED = 7, + }; + + public: + Record(); + virtual ~Record(); + + static std::optional FromStream(Stream& stream); + std::vector ToBytes(uint8_t flags = 0) const; + + TypeNameFormat GetTNF() const; + const std::vector& GetID() const; + const std::vector& GetType() const; + const std::vector& GetPayload() const; + + void SetTNF(TypeNameFormat tnf); + void SetID(const std::span& id); + void SetType(const std::span& type); + void SetPayload(const std::span& payload); + + bool IsLast() const; + bool IsShort() const; + + private: + uint8_t mFlags; + TypeNameFormat mTNF; + std::vector mID; + std::vector mType; + std::vector mPayload; + }; + + class Message + { + public: + Message(); + virtual ~Message(); + + static std::optional FromBytes(const std::span& data); + std::vector ToBytes() const; + + Record& operator[](int i) { return mRecords[i]; } + const Record& operator[](int i) const { return mRecords[i]; } + + void append(const Record& r); + + auto begin() { return mRecords.begin(); } + auto end() { return mRecords.end(); } + auto begin() const { return mRecords.begin(); } + auto end() const { return mRecords.end(); } + + private: + std::vector mRecords; + }; + +} // namespace ndef diff --git a/src/Cafe/OS/libs/nfc/nfc.cpp b/src/Cafe/OS/libs/nfc/nfc.cpp new file mode 100644 index 00000000..21e9e91b --- /dev/null +++ b/src/Cafe/OS/libs/nfc/nfc.cpp @@ -0,0 +1,596 @@ +#include "Cafe/OS/common/OSCommon.h" +#include "Cafe/OS/RPL/rpl.h" +#include "Cafe/OS/libs/nfc/nfc.h" +#include "Cafe/OS/libs/nn_nfp/nn_nfp.h" +#include "Common/FileStream.h" + +#include "TagV0.h" +#include "ndef.h" + +// TODO move errors to header and allow ntag to convert them + +#define NFC_MODE_INVALID -1 +#define NFC_MODE_IDLE 0 +#define NFC_MODE_ACTIVE 1 + +#define NFC_STATE_UNINITIALIZED 0x0 +#define NFC_STATE_INITIALIZED 0x1 +#define NFC_STATE_IDLE 0x2 +#define NFC_STATE_READ 0x3 +#define NFC_STATE_WRITE 0x4 +#define NFC_STATE_ABORT 0x5 +#define NFC_STATE_FORMAT 0x6 +#define NFC_STATE_SET_READ_ONLY 0x7 +#define NFC_STATE_TAG_PRESENT 0x8 +#define NFC_STATE_DETECT 0x9 +#define NFC_STATE_RAW 0xA + +#define NFC_STATUS_COMMAND_COMPLETE 0x1 +#define NFC_STATUS_READY 0x2 +#define NFC_STATUS_HAS_TAG 0x4 + +namespace nfc +{ + struct NFCContext + { + bool isInitialized; + uint32 state; + sint32 mode; + bool hasTag; + + uint32 nfcStatus; + std::chrono::time_point discoveryTimeout; + + MPTR tagDetectCallback; + void* tagDetectContext; + MPTR abortCallback; + void* abortContext; + MPTR rawCallback; + void* rawContext; + MPTR readCallback; + void* readContext; + MPTR writeCallback; + void* writeContext; + MPTR getTagInfoCallback; + + SysAllocator tagInfo; + + fs::path tagPath; + std::shared_ptr tag; + + ndef::Message writeMessage; + }; + NFCContext gNFCContexts[2]; + + sint32 NFCInit(uint32 chan) + { + return NFCInitEx(chan, 0); + } + + void __NFCClearContext(NFCContext* context) + { + context->isInitialized = false; + context->state = NFC_STATE_UNINITIALIZED; + context->mode = NFC_MODE_IDLE; + context->hasTag = false; + + context->nfcStatus = NFC_STATUS_READY; + context->discoveryTimeout = {}; + + context->tagDetectCallback = MPTR_NULL; + context->tagDetectContext = nullptr; + context->abortCallback = MPTR_NULL; + context->abortContext = nullptr; + context->rawCallback = MPTR_NULL; + context->rawContext = nullptr; + context->readCallback = MPTR_NULL; + context->readContext = nullptr; + context->writeCallback = MPTR_NULL; + context->writeContext = nullptr; + + context->tagPath = ""; + context->tag = {}; + } + + sint32 NFCInitEx(uint32 chan, uint32 powerMode) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + __NFCClearContext(ctx); + ctx->isInitialized = true; + ctx->state = NFC_STATE_INITIALIZED; + + return 0; + } + + sint32 NFCShutdown(uint32 chan) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + __NFCClearContext(ctx); + + return 0; + } + + bool NFCIsInit(uint32 chan) + { + cemu_assert(chan < 2); + + return gNFCContexts[chan].isInitialized; + } + + void __NFCHandleRead(uint32 chan) + { + NFCContext* ctx = &gNFCContexts[chan]; + + ctx->state = NFC_STATE_IDLE; + + sint32 result; + StackAllocator uid; + bool readOnly = false; + uint32 dataSize = 0; + StackAllocator data; + uint32 lockedDataSize = 0; + StackAllocator lockedData; + + if (ctx->tag) + { + // Try to parse ndef message + auto ndefMsg = ndef::Message::FromBytes(ctx->tag->GetNDEFData()); + if (ndefMsg) + { + // Look for the unknown TNF which contains the data we care about + for (const auto& rec : *ndefMsg) + { + if (rec.GetTNF() == ndef::Record::NDEF_TNF_UNKNOWN) { + dataSize = rec.GetPayload().size(); + cemu_assert(dataSize < 0x200); + memcpy(data.GetPointer(), rec.GetPayload().data(), dataSize); + break; + } + } + + if (dataSize) + { + // Get locked data + lockedDataSize = ctx->tag->GetLockedArea().size(); + memcpy(lockedData.GetPointer(), ctx->tag->GetLockedArea().data(), lockedDataSize); + + // Fill in uid + memcpy(uid.GetPointer(), ctx->tag->GetUIDBlock().data(), sizeof(NFCUid)); + + result = 0; + } + else + { + result = -0xBFE; + } + } + else + { + result = -0xBFE; + } + + // Clear tag status after read + // TODO this is not really nice here + ctx->nfcStatus &= ~NFC_STATUS_HAS_TAG; + ctx->tag = {}; + } + else + { + result = -0x1DD; + } + + PPCCoreCallback(ctx->readCallback, chan, result, uid.GetPointer(), readOnly, dataSize, data.GetPointer(), lockedDataSize, lockedData.GetPointer(), ctx->readContext); + } + + void __NFCHandleWrite(uint32 chan) + { + NFCContext* ctx = &gNFCContexts[chan]; + + ctx->state = NFC_STATE_IDLE; + + // TODO write to file + + PPCCoreCallback(ctx->writeCallback, chan, 0, ctx->writeContext); + } + + void __NFCHandleAbort(uint32 chan) + { + NFCContext* ctx = &gNFCContexts[chan]; + + ctx->state = NFC_STATE_IDLE; + + PPCCoreCallback(ctx->abortCallback, chan, 0, ctx->abortContext); + } + + void __NFCHandleRaw(uint32 chan) + { + NFCContext* ctx = &gNFCContexts[chan]; + + ctx->state = NFC_STATE_IDLE; + + sint32 result; + if (ctx->nfcStatus & NFC_STATUS_HAS_TAG) + { + result = 0; + } + else + { + result = -0x9DD; + } + + // We don't actually send any commands/responses + uint32 responseSize = 0; + void* responseData = nullptr; + + PPCCoreCallback(ctx->rawCallback, chan, result, responseSize, responseData, ctx->rawContext); + } + + void NFCProc(uint32 chan) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!ctx->isInitialized) + { + return; + } + + // Check if the detect callback should be called + if (ctx->nfcStatus & NFC_STATUS_HAS_TAG) + { + if (!ctx->hasTag && ctx->state > NFC_STATE_IDLE && ctx->state != NFC_STATE_ABORT) + { + if (ctx->tagDetectCallback) + { + PPCCoreCallback(ctx->tagDetectCallback, chan, true, ctx->tagDetectContext); + } + + ctx->hasTag = true; + } + } + else + { + if (ctx->hasTag && ctx->state == NFC_STATE_IDLE) + { + if (ctx->tagDetectCallback) + { + PPCCoreCallback(ctx->tagDetectCallback, chan, false, ctx->tagDetectContext); + } + + ctx->hasTag = false; + } + } + + switch (ctx->state) + { + case NFC_STATE_INITIALIZED: + ctx->state = NFC_STATE_IDLE; + break; + case NFC_STATE_IDLE: + break; + case NFC_STATE_READ: + // Do we have a tag or did the timeout expire? + if ((ctx->nfcStatus & NFC_STATUS_HAS_TAG) || ctx->discoveryTimeout < std::chrono::system_clock::now()) + { + __NFCHandleRead(chan); + } + break; + case NFC_STATE_WRITE: + __NFCHandleWrite(chan); + break; + case NFC_STATE_ABORT: + __NFCHandleAbort(chan); + break; + case NFC_STATE_RAW: + // Do we have a tag or did the timeout expire? + if ((ctx->nfcStatus & NFC_STATUS_HAS_TAG) || ctx->discoveryTimeout < std::chrono::system_clock::now()) + { + __NFCHandleRaw(chan); + } + break; + } + } + + sint32 NFCGetMode(uint32 chan) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!NFCIsInit(chan) || ctx->state == NFC_STATE_UNINITIALIZED) + { + return NFC_MODE_INVALID; + } + + return ctx->mode; + } + + sint32 NFCSetMode(uint32 chan, sint32 mode) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!NFCIsInit(chan)) + { + return -0xAE0; + } + + if (ctx->state == NFC_STATE_UNINITIALIZED) + { + return -0xADF; + } + + ctx->mode = mode; + + return 0; + } + + void NFCSetTagDetectCallback(uint32 chan, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + ctx->tagDetectCallback = callback; + ctx->tagDetectContext = context; + } + + sint32 NFCAbort(uint32 chan, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!NFCIsInit(chan)) + { + return -0x6E0; + } + + if (ctx->state <= NFC_STATE_IDLE) + { + return -0x6DF; + } + + ctx->state = NFC_STATE_ABORT; + ctx->abortCallback = callback; + ctx->abortContext = context; + + return 0; + } + + void __NFCGetTagInfoCallback(PPCInterpreter_t* hCPU) + { + ppcDefineParamU32(chan, 0); + ppcDefineParamS32(error, 1); + ppcDefineParamU32(responseSize, 2); + ppcDefineParamPtr(responseData, void, 3); + ppcDefineParamPtr(context, void, 4); + + NFCContext* ctx = &gNFCContexts[chan]; + + // TODO convert error + error = error; + if (error == 0 && ctx->tag) + { + // this is usually parsed from response data + ctx->tagInfo->uidSize = sizeof(NFCUid); + memcpy(ctx->tagInfo->uid, ctx->tag->GetUIDBlock().data(), ctx->tagInfo->uidSize); + ctx->tagInfo->technology = NFC_TECHNOLOGY_A; + ctx->tagInfo->protocol = NFC_PROTOCOL_T1T; + } + + PPCCoreCallback(ctx->getTagInfoCallback, chan, error, ctx->tagInfo.GetPtr(), context); + osLib_returnFromFunction(hCPU, 0); + } + + sint32 NFCGetTagInfo(uint32 chan, uint32 discoveryTimeout, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + // Forward this request to nn_nfp, if the title initialized it + // TODO integrate nn_nfp/ntag/nfc + if (nnNfp_isInitialized()) + { + return nn::nfp::NFCGetTagInfo(chan, discoveryTimeout, callback, context); + } + + NFCContext* ctx = &gNFCContexts[chan]; + + ctx->getTagInfoCallback = callback; + + sint32 result = NFCSendRawData(chan, true, discoveryTimeout, 1000U, 0, 0, nullptr, RPLLoader_MakePPCCallable(__NFCGetTagInfoCallback), context); + return result; // TODO convert result + } + + sint32 NFCSendRawData(uint32 chan, bool startDiscovery, uint32 discoveryTimeout, uint32 commandTimeout, uint32 commandSize, uint32 responseSize, void* commandData, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!NFCIsInit(chan)) + { + return -0x9E0; + } + + // Only allow discovery + if (!startDiscovery) + { + return -0x9DC; + } + + if (NFCGetMode(chan) == NFC_MODE_ACTIVE && NFCSetMode(chan, NFC_MODE_IDLE) < 0) + { + return -0x9DC; + } + + if (ctx->state != NFC_STATE_IDLE) + { + return -0x9DF; + } + + ctx->state = NFC_STATE_RAW; + ctx->rawCallback = callback; + ctx->rawContext = context; + + // If the discoveryTimeout is 0, no timeout + if (discoveryTimeout == 0) + { + ctx->discoveryTimeout = std::chrono::time_point::max(); + } + else + { + ctx->discoveryTimeout = std::chrono::system_clock::now() + std::chrono::milliseconds(discoveryTimeout); + } + + return 0; + } + + sint32 NFCRead(uint32 chan, uint32 discoveryTimeout, NFCUid* uid, NFCUid* uidMask, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!NFCIsInit(chan)) + { + return -0x1E0; + } + + if (NFCGetMode(chan) == NFC_MODE_ACTIVE && NFCSetMode(chan, NFC_MODE_IDLE) < 0) + { + return -0x1DC; + } + + if (ctx->state != NFC_STATE_IDLE) + { + return -0x1DF; + } + + cemuLog_log(LogType::NFC, "starting read"); + + ctx->state = NFC_STATE_READ; + ctx->readCallback = callback; + ctx->readContext = context; + + // If the discoveryTimeout is 0, no timeout + if (discoveryTimeout == 0) + { + ctx->discoveryTimeout = std::chrono::time_point::max(); + } + else + { + ctx->discoveryTimeout = std::chrono::system_clock::now() + std::chrono::milliseconds(discoveryTimeout); + } + + // TODO uid filter? + + return 0; + } + + sint32 NFCWrite(uint32 chan, uint32 discoveryTimeout, NFCUid* uid, NFCUid* uidMask, uint32 size, void* data, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + NFCContext* ctx = &gNFCContexts[chan]; + + if (!NFCIsInit(chan)) + { + return -0x2e0; + } + + if (NFCGetMode(chan) == NFC_MODE_ACTIVE && NFCSetMode(chan, NFC_MODE_IDLE) < 0) + { + return -0x2dc; + } + + if (ctx->state != NFC_STATE_IDLE) + { + return -0x1df; + } + + // Create unknown record which contains the rw area + ndef::Record rec; + rec.SetTNF(ndef::Record::NDEF_TNF_UNKNOWN); + rec.SetPayload(std::span(reinterpret_cast(data), size)); + + // Create ndef message which contains the record + ndef::Message msg; + msg.append(rec); + ctx->writeMessage = msg; + + ctx->state = NFC_STATE_WRITE; + ctx->writeCallback = callback; + ctx->writeContext = context; + + // If the discoveryTimeout is 0, no timeout + if (discoveryTimeout == 0) + { + ctx->discoveryTimeout = std::chrono::time_point::max(); + } + else + { + ctx->discoveryTimeout = std::chrono::system_clock::now() + std::chrono::milliseconds(discoveryTimeout); + } + + // TODO uid filter? + + return 0; + } + + void Initialize() + { + cafeExportRegister("nfc", NFCInit, LogType::NFC); + cafeExportRegister("nfc", NFCInitEx, LogType::NFC); + cafeExportRegister("nfc", NFCShutdown, LogType::NFC); + cafeExportRegister("nfc", NFCIsInit, LogType::NFC); + cafeExportRegister("nfc", NFCProc, LogType::NFC); + cafeExportRegister("nfc", NFCGetMode, LogType::NFC); + cafeExportRegister("nfc", NFCSetMode, LogType::NFC); + cafeExportRegister("nfc", NFCSetTagDetectCallback, LogType::NFC); + cafeExportRegister("nfc", NFCGetTagInfo, LogType::NFC); + cafeExportRegister("nfc", NFCSendRawData, LogType::NFC); + cafeExportRegister("nfc", NFCAbort, LogType::NFC); + cafeExportRegister("nfc", NFCRead, LogType::NFC); + cafeExportRegister("nfc", NFCWrite, LogType::NFC); + } + + bool TouchTagFromFile(const fs::path& filePath, uint32* nfcError) + { + // Forward this request to nn_nfp, if the title initialized it + // TODO integrate nn_nfp/ntag/nfc + if (nnNfp_isInitialized()) + { + return nnNfp_touchNfcTagFromFile(filePath, nfcError); + } + + NFCContext* ctx = &gNFCContexts[0]; + + auto nfcData = FileStream::LoadIntoMemory(filePath); + if (!nfcData) + { + *nfcError = NFC_ERROR_NO_ACCESS; + return false; + } + + ctx->tag = TagV0::FromBytes(std::as_bytes(std::span(nfcData->data(), nfcData->size()))); + if (!ctx->tag) + { + *nfcError = NFC_ERROR_INVALID_FILE_FORMAT; + return false; + } + + ctx->nfcStatus |= NFC_STATUS_HAS_TAG; + ctx->tagPath = filePath; + + *nfcError = NFC_ERROR_NONE; + return true; + } +} diff --git a/src/Cafe/OS/libs/nfc/nfc.h b/src/Cafe/OS/libs/nfc/nfc.h new file mode 100644 index 00000000..2ebdd2a4 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/nfc.h @@ -0,0 +1,62 @@ +#pragma once + +// CEMU NFC error codes +#define NFC_ERROR_NONE (0) +#define NFC_ERROR_NO_ACCESS (1) +#define NFC_ERROR_INVALID_FILE_FORMAT (2) + +#define NFC_PROTOCOL_T1T 0x1 +#define NFC_PROTOCOL_T2T 0x2 + +#define NFC_TECHNOLOGY_A 0x0 +#define NFC_TECHNOLOGY_B 0x1 +#define NFC_TECHNOLOGY_F 0x2 + +namespace nfc +{ + struct NFCUid + { + /* +0x00 */ uint8 uid[7]; + }; + static_assert(sizeof(NFCUid) == 0x7); + + struct NFCTagInfo + { + /* +0x00 */ uint8 uidSize; + /* +0x01 */ uint8 uid[10]; + /* +0x0B */ uint8 technology; + /* +0x0C */ uint8 protocol; + /* +0x0D */ uint8 reserved[0x20]; + }; + static_assert(sizeof(NFCTagInfo) == 0x2D); + + sint32 NFCInit(uint32 chan); + + sint32 NFCInitEx(uint32 chan, uint32 powerMode); + + sint32 NFCShutdown(uint32 chan); + + bool NFCIsInit(uint32 chan); + + void NFCProc(uint32 chan); + + sint32 NFCGetMode(uint32 chan); + + sint32 NFCSetMode(uint32 chan, sint32 mode); + + void NFCSetTagDetectCallback(uint32 chan, MPTR callback, void* context); + + sint32 NFCGetTagInfo(uint32 chan, uint32 discoveryTimeout, MPTR callback, void* context); + + sint32 NFCSendRawData(uint32 chan, bool startDiscovery, uint32 discoveryTimeout, uint32 commandTimeout, uint32 commandSize, uint32 responseSize, void* commandData, MPTR callback, void* context); + + sint32 NFCAbort(uint32 chan, MPTR callback, void* context); + + sint32 NFCRead(uint32 chan, uint32 discoveryTimeout, NFCUid* uid, NFCUid* uidMask, MPTR callback, void* context); + + sint32 NFCWrite(uint32 chan, uint32 discoveryTimeout, NFCUid* uid, NFCUid* uidMask, uint32 size, void* data, MPTR callback, void* context); + + void Initialize(); + + bool TouchTagFromFile(const fs::path& filePath, uint32* nfcError); +} diff --git a/src/Cafe/OS/libs/nfc/stream.cpp b/src/Cafe/OS/libs/nfc/stream.cpp new file mode 100644 index 00000000..73c2880f --- /dev/null +++ b/src/Cafe/OS/libs/nfc/stream.cpp @@ -0,0 +1,201 @@ +#include "stream.h" + +#include + +Stream::Stream(std::endian endianness) + : mError(ERROR_OK), mEndianness(endianness) +{ +} + +Stream::~Stream() +{ +} + +Stream::Error Stream::GetError() const +{ + return mError; +} + +void Stream::SetEndianness(std::endian endianness) +{ + mEndianness = endianness; +} + +std::endian Stream::GetEndianness() const +{ + return mEndianness; +} + +Stream& Stream::operator>>(bool& val) +{ + std::uint8_t i; + *this >> i; + val = !!i; + + return *this; +} + +Stream& Stream::operator>>(float& val) +{ + std::uint32_t i; + *this >> i; + val = std::bit_cast(i); + + return *this; +} + +Stream& Stream::operator>>(double& val) +{ + std::uint64_t i; + *this >> i; + val = std::bit_cast(i); + + return *this; +} + +Stream& Stream::operator<<(bool val) +{ + std::uint8_t i = val; + *this >> i; + + return *this; +} + +Stream& Stream::operator<<(float val) +{ + std::uint32_t i = std::bit_cast(val); + *this >> i; + + return *this; +} + +Stream& Stream::operator<<(double val) +{ + std::uint64_t i = std::bit_cast(val); + *this >> i; + + return *this; +} + +void Stream::SetError(Error error) +{ + mError = error; +} + +bool Stream::NeedsSwap() +{ + return mEndianness != std::endian::native; +} + +VectorStream::VectorStream(std::vector& vector, std::endian endianness) + : Stream(endianness), mVector(vector), mPosition(0) +{ +} + +VectorStream::~VectorStream() +{ +} + +std::size_t VectorStream::Read(const std::span& data) +{ + if (data.size() > GetRemaining()) + { + SetError(ERROR_READ_FAILED); + std::fill(data.begin(), data.end(), std::byte(0)); + return 0; + } + + std::copy_n(mVector.get().begin() + mPosition, data.size(), data.begin()); + mPosition += data.size(); + return data.size(); +} + +std::size_t VectorStream::Write(const std::span& data) +{ + // Resize vector if not enough bytes remain + if (mPosition + data.size() > mVector.get().size()) + { + mVector.get().resize(mPosition + data.size()); + } + + std::copy(data.begin(), data.end(), mVector.get().begin() + mPosition); + mPosition += data.size(); + return data.size(); +} + +bool VectorStream::SetPosition(std::size_t position) +{ + if (position >= mVector.get().size()) + { + return false; + } + + mPosition = position; + return true; +} + +std::size_t VectorStream::GetPosition() const +{ + return mPosition; +} + +std::size_t VectorStream::GetRemaining() const +{ + return mVector.get().size() - mPosition; +} + +SpanStream::SpanStream(std::span span, std::endian endianness) + : Stream(endianness), mSpan(std::move(span)), mPosition(0) +{ +} + +SpanStream::~SpanStream() +{ +} + +std::size_t SpanStream::Read(const std::span& data) +{ + if (data.size() > GetRemaining()) + { + SetError(ERROR_READ_FAILED); + std::fill(data.begin(), data.end(), std::byte(0)); + return 0; + } + + std::copy_n(mSpan.begin() + mPosition, data.size(), data.begin()); + mPosition += data.size(); + return data.size(); +} + +std::size_t SpanStream::Write(const std::span& data) +{ + // Cannot write to const span + SetError(ERROR_WRITE_FAILED); + return 0; +} + +bool SpanStream::SetPosition(std::size_t position) +{ + if (position >= mSpan.size()) + { + return false; + } + + mPosition = position; + return true; +} + +std::size_t SpanStream::GetPosition() const +{ + return mPosition; +} + +std::size_t SpanStream::GetRemaining() const +{ + if (mPosition > mSpan.size()) + { + return 0; + } + + return mSpan.size() - mPosition; +} diff --git a/src/Cafe/OS/libs/nfc/stream.h b/src/Cafe/OS/libs/nfc/stream.h new file mode 100644 index 00000000..e666b480 --- /dev/null +++ b/src/Cafe/OS/libs/nfc/stream.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include + +#include "Common/precompiled.h" + +class Stream +{ +public: + enum Error + { + ERROR_OK, + ERROR_READ_FAILED, + ERROR_WRITE_FAILED, + }; + +public: + Stream(std::endian endianness = std::endian::native); + virtual ~Stream(); + + Error GetError() const; + + void SetEndianness(std::endian endianness); + std::endian GetEndianness() const; + + virtual std::size_t Read(const std::span& data) = 0; + virtual std::size_t Write(const std::span& data) = 0; + + virtual bool SetPosition(std::size_t position) = 0; + virtual std::size_t GetPosition() const = 0; + + virtual std::size_t GetRemaining() const = 0; + + // Stream read operators + template + Stream& operator>>(T& val) + { + val = 0; + if (Read(std::as_writable_bytes(std::span(std::addressof(val), 1))) == sizeof(val)) + { + if (NeedsSwap()) + { + if (sizeof(T) == 2) + { + val = _swapEndianU16(val); + } + else if (sizeof(T) == 4) + { + val = _swapEndianU32(val); + } + else if (sizeof(T) == 8) + { + val = _swapEndianU64(val); + } + } + } + + return *this; + } + Stream& operator>>(bool& val); + Stream& operator>>(float& val); + Stream& operator>>(double& val); + + // Stream write operators + template + Stream& operator<<(T val) + { + if (NeedsSwap()) + { + if (sizeof(T) == 2) + { + val = _swapEndianU16(val); + } + else if (sizeof(T) == 4) + { + val = _swapEndianU32(val); + } + else if (sizeof(T) == 8) + { + val = _swapEndianU64(val); + } + } + + Write(std::as_bytes(std::span(std::addressof(val), 1))); + return *this; + } + Stream& operator<<(bool val); + Stream& operator<<(float val); + Stream& operator<<(double val); + +protected: + void SetError(Error error); + + bool NeedsSwap(); + + Error mError; + std::endian mEndianness; +}; + +class VectorStream : public Stream +{ +public: + VectorStream(std::vector& vector, std::endian endianness = std::endian::native); + virtual ~VectorStream(); + + virtual std::size_t Read(const std::span& data) override; + virtual std::size_t Write(const std::span& data) override; + + virtual bool SetPosition(std::size_t position) override; + virtual std::size_t GetPosition() const override; + + virtual std::size_t GetRemaining() const override; + +private: + std::reference_wrapper> mVector; + std::size_t mPosition; +}; + +class SpanStream : public Stream +{ +public: + SpanStream(std::span span, std::endian endianness = std::endian::native); + virtual ~SpanStream(); + + virtual std::size_t Read(const std::span& data) override; + virtual std::size_t Write(const std::span& data) override; + + virtual bool SetPosition(std::size_t position) override; + virtual std::size_t GetPosition() const override; + + virtual std::size_t GetRemaining() const override; + +private: + std::span mSpan; + std::size_t mPosition; +}; diff --git a/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp b/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp index ad2ea203..10d9e7cb 100644 --- a/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp +++ b/src/Cafe/OS/libs/nn_nfp/nn_nfp.cpp @@ -293,41 +293,6 @@ void nnNfpExport_GetTagInfo(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_NFP, 0)); } -typedef struct -{ - /* +0x00 */ uint8 uidLength; - /* +0x01 */ uint8 uid[0xA]; - /* +0x0B */ uint8 ukn0B; - /* +0x0C */ uint8 ukn0C; - /* +0x0D */ uint8 ukn0D; - // more? -}NFCTagInfoCallbackParam_t; - -uint32 NFCGetTagInfo(uint32 index, uint32 timeout, MPTR functionPtr, void* userParam) -{ - cemuLog_log(LogType::NN_NFP, "NFCGetTagInfo({},{},0x{:08x},0x{:08x})", index, timeout, functionPtr, userParam ? memory_getVirtualOffsetFromPointer(userParam) : 0); - - - cemu_assert(index == 0); - - nnNfpLock(); - - StackAllocator _callbackParam; - NFCTagInfoCallbackParam_t* callbackParam = _callbackParam.GetPointer(); - - memset(callbackParam, 0x00, sizeof(NFCTagInfoCallbackParam_t)); - - memcpy(callbackParam->uid, nfp_data.amiiboProcessedData.uid, nfp_data.amiiboProcessedData.uidLength); - callbackParam->uidLength = (uint8)nfp_data.amiiboProcessedData.uidLength; - - PPCCoreCallback(functionPtr, index, 0, _callbackParam.GetPointer(), userParam); - - nnNfpUnlock(); - - - return 0; // 0 -> success -} - void nnNfpExport_Mount(PPCInterpreter_t* hCPU) { cemuLog_log(LogType::NN_NFP, "Mount()"); @@ -769,6 +734,16 @@ void nnNfp_unloadAmiibo() nnNfpUnlock(); } +bool nnNfp_isInitialized() +{ + return nfp_data.nfpIsInitialized; +} + +// CEMU NFC error codes +#define NFC_ERROR_NONE (0) +#define NFC_ERROR_NO_ACCESS (1) +#define NFC_ERROR_INVALID_FILE_FORMAT (2) + bool nnNfp_touchNfcTagFromFile(const fs::path& filePath, uint32* nfcError) { AmiiboRawNFCData rawData = { 0 }; @@ -960,6 +935,41 @@ void nnNfpExport_GetNfpState(PPCInterpreter_t* hCPU) namespace nn::nfp { + typedef struct + { + /* +0x00 */ uint8 uidLength; + /* +0x01 */ uint8 uid[0xA]; + /* +0x0B */ uint8 ukn0B; + /* +0x0C */ uint8 ukn0C; + /* +0x0D */ uint8 ukn0D; + // more? + }NFCTagInfoCallbackParam_t; + + uint32 NFCGetTagInfo(uint32 index, uint32 timeout, MPTR functionPtr, void* userParam) + { + cemuLog_log(LogType::NN_NFP, "NFCGetTagInfo({},{},0x{:08x},0x{:08x})", index, timeout, functionPtr, userParam ? memory_getVirtualOffsetFromPointer(userParam) : 0); + + + cemu_assert(index == 0); + + nnNfpLock(); + + StackAllocator _callbackParam; + NFCTagInfoCallbackParam_t* callbackParam = _callbackParam.GetPointer(); + + memset(callbackParam, 0x00, sizeof(NFCTagInfoCallbackParam_t)); + + memcpy(callbackParam->uid, nfp_data.amiiboProcessedData.uid, nfp_data.amiiboProcessedData.uidLength); + callbackParam->uidLength = (uint8)nfp_data.amiiboProcessedData.uidLength; + + PPCCoreCallback(functionPtr, index, 0, _callbackParam.GetPointer(), userParam); + + nnNfpUnlock(); + + + return 0; // 0 -> success + } + uint32 GetErrorCode(uint32 result) { uint32 level = (result >> 0x1b) & 3; @@ -1019,9 +1029,6 @@ 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); - - // NFC API - cafeExportRegister("nn_nfp", NFCGetTagInfo, LogType::Placeholder); } } diff --git a/src/Cafe/OS/libs/nn_nfp/nn_nfp.h b/src/Cafe/OS/libs/nn_nfp/nn_nfp.h index e8a1c55f..25b36cc9 100644 --- a/src/Cafe/OS/libs/nn_nfp/nn_nfp.h +++ b/src/Cafe/OS/libs/nn_nfp/nn_nfp.h @@ -2,12 +2,15 @@ namespace nn::nfp { + uint32 NFCGetTagInfo(uint32 index, uint32 timeout, MPTR functionPtr, void* userParam); + void load(); } void nnNfp_load(); void nnNfp_update(); +bool nnNfp_isInitialized(); bool nnNfp_touchNfcTagFromFile(const fs::path& filePath, uint32* nfcError); #define NFP_STATE_NONE (0) @@ -18,8 +21,3 @@ bool nnNfp_touchNfcTagFromFile(const fs::path& filePath, uint32* nfcError); #define NFP_STATE_RW_MOUNT (5) #define NFP_STATE_UNEXPECTED (6) #define NFP_STATE_RW_MOUNT_ROM (7) - -// CEMU NFC error codes -#define NFC_ERROR_NONE (0) -#define NFC_ERROR_NO_ACCESS (1) -#define NFC_ERROR_INVALID_FILE_FORMAT (2) diff --git a/src/Cafe/OS/libs/ntag/ntag.cpp b/src/Cafe/OS/libs/ntag/ntag.cpp new file mode 100644 index 00000000..8bdbb66f --- /dev/null +++ b/src/Cafe/OS/libs/ntag/ntag.cpp @@ -0,0 +1,438 @@ +#include "Cafe/OS/common/OSCommon.h" +#include "Cafe/OS/RPL/rpl.h" +#include "Cafe/OS/libs/ntag/ntag.h" +#include "Cafe/OS/libs/nfc/nfc.h" +#include "Cafe/OS/libs/coreinit/coreinit_IPC.h" +#include "Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.h" + +namespace ntag +{ + struct NTAGWriteData + { + + }; + NTAGWriteData gWriteData[2]; + + bool ccrNfcOpened = false; + IOSDevHandle gCcrNfcHandle; + + NTAGFormatSettings gFormatSettings; + + MPTR gDetectCallbacks[2]; + MPTR gAbortCallbacks[2]; + MPTR gReadCallbacks[2]; + MPTR gWriteCallbacks[2]; + + sint32 __NTAGConvertNFCError(sint32 error) + { + // TODO + return error; + } + + sint32 NTAGInit(uint32 chan) + { + return NTAGInitEx(chan); + } + + sint32 NTAGInitEx(uint32 chan) + { + sint32 result = nfc::NFCInitEx(chan, 1); + return __NTAGConvertNFCError(result); + } + + sint32 NTAGShutdown(uint32 chan) + { + sint32 result = nfc::NFCShutdown(chan); + + if (ccrNfcOpened) + { + coreinit::IOS_Close(gCcrNfcHandle); + ccrNfcOpened = false; + } + + gDetectCallbacks[chan] = MPTR_NULL; + gAbortCallbacks[chan] = MPTR_NULL; + gReadCallbacks[chan] = MPTR_NULL; + gWriteCallbacks[chan] = MPTR_NULL; + + return __NTAGConvertNFCError(result); + } + + bool NTAGIsInit(uint32 chan) + { + return nfc::NFCIsInit(chan); + } + + void NTAGProc(uint32 chan) + { + nfc::NFCProc(chan); + } + + void NTAGSetFormatSettings(NTAGFormatSettings* formatSettings) + { + gFormatSettings.version = formatSettings->version; + gFormatSettings.makerCode = _swapEndianU32(formatSettings->makerCode); + gFormatSettings.indentifyCode = _swapEndianU32(formatSettings->indentifyCode); + } + + void __NTAGDetectCallback(PPCInterpreter_t* hCPU) + { + ppcDefineParamU32(chan, 0); + ppcDefineParamU32(hasTag, 1); + ppcDefineParamPtr(context, void, 2); + + cemuLog_log(LogType::NTAG, "__NTAGDetectCallback: {} {} {}", chan, hasTag, context); + + PPCCoreCallback(gDetectCallbacks[chan], chan, hasTag, context); + + osLib_returnFromFunction(hCPU, 0); + } + + void NTAGSetTagDetectCallback(uint32 chan, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + gDetectCallbacks[chan] = callback; + nfc::NFCSetTagDetectCallback(chan, RPLLoader_MakePPCCallable(__NTAGDetectCallback), context); + } + + void __NTAGAbortCallback(PPCInterpreter_t* hCPU) + { + ppcDefineParamU32(chan, 0); + ppcDefineParamS32(error, 1); + ppcDefineParamPtr(context, void, 2); + + PPCCoreCallback(gAbortCallbacks[chan], chan, __NTAGConvertNFCError(error), context); + + osLib_returnFromFunction(hCPU, 0); + } + + sint32 NTAGAbort(uint32 chan, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + // TODO is it normal that Rumble U calls this? + + gAbortCallbacks[chan] = callback; + sint32 result = nfc::NFCAbort(chan, RPLLoader_MakePPCCallable(__NTAGAbortCallback), context); + return __NTAGConvertNFCError(result); + } + + bool __NTAGRawDataToNfcData(iosu::ccr_nfc::CCRNFCCryptData* raw, iosu::ccr_nfc::CCRNFCCryptData* nfc) + { + memcpy(nfc, raw, sizeof(iosu::ccr_nfc::CCRNFCCryptData)); + + if (raw->version == 0) + { + nfc->version = 0; + nfc->dataSize = 0x1C8; + nfc->seedOffset = 0x25; + nfc->keyGenSaltOffset = 0x1A8; + nfc->uuidOffset = 0x198; + nfc->unfixedInfosOffset = 0x28; + nfc->unfixedInfosSize = 0x120; + nfc->lockedSecretOffset = 0x168; + nfc->lockedSecretSize = 0x30; + nfc->unfixedInfosHmacOffset = 0; + nfc->lockedSecretHmacOffset = 0x148; + } + else if (raw->version == 2) + { + nfc->version = 2; + nfc->dataSize = 0x208; + nfc->seedOffset = 0x29; + nfc->keyGenSaltOffset = 0x1E8; + nfc->uuidOffset = 0x1D4; + nfc->unfixedInfosOffset = 0x2C; + nfc->unfixedInfosSize = 0x188; + nfc->lockedSecretOffset = 0x1DC; + nfc->lockedSecretSize = 0; + nfc->unfixedInfosHmacOffset = 0x8; + nfc->lockedSecretHmacOffset = 0x1B4; + + memcpy(nfc->data + 0x1d4, raw->data, 0x8); + memcpy(nfc->data, raw->data + 0x8, 0x8); + memcpy(nfc->data + 0x28, raw->data + 0x10, 0x4); + memcpy(nfc->data + nfc->unfixedInfosOffset, raw->data + 0x14, 0x20); + memcpy(nfc->data + nfc->lockedSecretHmacOffset, raw->data + 0x34, 0x20); + memcpy(nfc->data + nfc->lockedSecretOffset, raw->data + 0x54, 0xC); + memcpy(nfc->data + nfc->keyGenSaltOffset, raw->data + 0x60, 0x20); + memcpy(nfc->data + nfc->unfixedInfosHmacOffset, raw->data + 0x80, 0x20); + memcpy(nfc->data + nfc->unfixedInfosOffset + 0x20, raw->data + 0xa0, 0x168); + memcpy(nfc->data + 0x208, raw->data + 0x208, 0x14); + } + else + { + return false; + } + + return true; + } + + bool __NTAGNfcDataToRawData(iosu::ccr_nfc::CCRNFCCryptData* nfc, iosu::ccr_nfc::CCRNFCCryptData* raw) + { + memcpy(raw, nfc, sizeof(iosu::ccr_nfc::CCRNFCCryptData)); + + if (nfc->version == 0) + { + raw->version = 0; + raw->dataSize = 0x1C8; + raw->seedOffset = 0x25; + raw->keyGenSaltOffset = 0x1A8; + raw->uuidOffset = 0x198; + raw->unfixedInfosOffset = 0x28; + raw->unfixedInfosSize = 0x120; + raw->lockedSecretOffset = 0x168; + raw->lockedSecretSize = 0x30; + raw->unfixedInfosHmacOffset = 0; + raw->lockedSecretHmacOffset = 0x148; + } + else if (nfc->version == 2) + { + raw->version = 2; + raw->dataSize = 0x208; + raw->seedOffset = 0x11; + raw->keyGenSaltOffset = 0x60; + raw->uuidOffset = 0; + raw->unfixedInfosOffset = 0x14; + raw->unfixedInfosSize = 0x188; + raw->lockedSecretOffset = 0x54; + raw->lockedSecretSize = 0xC; + raw->unfixedInfosHmacOffset = 0x80; + raw->lockedSecretHmacOffset = 0x34; + + memcpy(raw->data + 0x8, nfc->data, 0x8); + memcpy(raw->data + raw->unfixedInfosHmacOffset, nfc->data + 0x8, 0x20); + memcpy(raw->data + 0x10, nfc->data + 0x28, 0x4); + memcpy(raw->data + raw->unfixedInfosOffset, nfc->data + 0x2C, 0x20); + memcpy(raw->data + 0xa0, nfc->data + 0x4C, 0x168); + memcpy(raw->data + raw->lockedSecretHmacOffset, nfc->data + 0x1B4, 0x20); + memcpy(raw->data + raw->uuidOffset, nfc->data + 0x1D4, 0x8); + memcpy(raw->data + raw->lockedSecretOffset, nfc->data + 0x1DC, 0xC); + memcpy(raw->data + raw->keyGenSaltOffset, nfc->data + 0x1E8, 0x20); + memcpy(raw->data + 0x208, nfc->data + 0x208, 0x14); + } + else + { + return false; + } + + return true; + } + + sint32 __NTAGDecryptData(void* decryptedData, void* rawData) + { + StackAllocator nfcRawData, nfcInData, nfcOutData; + + if (!ccrNfcOpened) + { + gCcrNfcHandle = coreinit::IOS_Open("/dev/ccr_nfc", 0); + } + + // Prepare nfc buffer + nfcRawData->version = 0; + memcpy(nfcRawData->data, rawData, 0x1C8); + __NTAGRawDataToNfcData(nfcRawData.GetPointer(), nfcInData.GetPointer()); + + // Decrypt + sint32 result = coreinit::IOS_Ioctl(gCcrNfcHandle, 2, nfcInData.GetPointer(), sizeof(iosu::ccr_nfc::CCRNFCCryptData), nfcOutData.GetPointer(), sizeof(iosu::ccr_nfc::CCRNFCCryptData)); + + // Unpack nfc buffer + __NTAGNfcDataToRawData(nfcOutData.GetPointer(), nfcRawData.GetPointer()); + memcpy(decryptedData, nfcRawData->data, 0x1C8); + + // Convert result + if (result == CCR_NFC_INVALID_UNFIXED_INFOS) + { + return -0x2708; + } + else if (result == CCR_NFC_INVALID_LOCKED_SECRET) + { + return -0x2707; + } + + return result; + } + + sint32 __NTAGValidateHeaders(NTAGNoftHeader* noftHeader, NTAGInfoHeader* infoHeader, NTAGAreaHeader* rwHeader, NTAGAreaHeader* roHeader) + { + // TODO + return 0; + } + + sint32 __NTAGParseHeaders(const uint8* data, NTAGNoftHeader* noftHeader, NTAGInfoHeader* infoHeader, NTAGAreaHeader* rwHeader, NTAGAreaHeader* roHeader) + { + memcpy(noftHeader, data + 0x20, sizeof(NTAGNoftHeader)); + memcpy(infoHeader, data + 0x198, sizeof(NTAGInfoHeader)); + memcpy(rwHeader, data + _swapEndianU16(infoHeader->rwHeaderOffset), sizeof(NTAGAreaHeader)); + memcpy(roHeader, data + _swapEndianU16(infoHeader->roHeaderOffset), sizeof(NTAGAreaHeader)); + return __NTAGValidateHeaders(noftHeader, infoHeader, rwHeader, roHeader); + } + + sint32 __NTAGParseData(void* rawData, void* rwData, void* roData, nfc::NFCUid* uid, uint32 lockedDataSize, NTAGNoftHeader* noftHeader, NTAGInfoHeader* infoHeader, NTAGAreaHeader* rwHeader, NTAGAreaHeader* roHeader) + { + uint8 decryptedData[0x1C8]; + sint32 result = __NTAGDecryptData(decryptedData, rawData); + if (result < 0) + { + return result; + } + + result = __NTAGParseHeaders(decryptedData, noftHeader, infoHeader, rwHeader, roHeader); + if (result < 0) + { + return result; + } + + if (_swapEndianU16(roHeader->size) + 0x70 != lockedDataSize) + { + cemuLog_log(LogType::Force, "Invalid locked area size"); + return -0x270C; + } + + if (memcmp(infoHeader->uid.uid, uid->uid, sizeof(nfc::NFCUid)) != 0) + { + cemuLog_log(LogType::Force, "UID mismatch"); + return -0x270B; + } + + cemu_assert(_swapEndianU16(rwHeader->offset) + _swapEndianU16(rwHeader->size) < 0x200); + cemu_assert(_swapEndianU16(roHeader->offset) + _swapEndianU16(roHeader->size) < 0x200); + + memcpy(rwData, decryptedData + _swapEndianU16(rwHeader->offset), _swapEndianU16(rwHeader->size)); + memcpy(roData, decryptedData + _swapEndianU16(roHeader->offset), _swapEndianU16(roHeader->size)); + + return 0; + } + + void __NTAGReadCallback(PPCInterpreter_t* hCPU) + { + ppcDefineParamU32(chan, 0); + ppcDefineParamS32(error, 1); + ppcDefineParamPtr(uid, nfc::NFCUid, 2); + ppcDefineParamU32(readOnly, 3); + ppcDefineParamU32(dataSize, 4); + ppcDefineParamPtr(data, void, 5); + ppcDefineParamU32(lockedDataSize, 6); + ppcDefineParamPtr(lockedData, void, 7); + ppcDefineParamPtr(context, void, 8); + + uint8 rawData[0x1C8]; + StackAllocator readResult; + StackAllocator rwData; + StackAllocator roData; + NTAGNoftHeader noftHeader; + NTAGInfoHeader infoHeader; + NTAGAreaHeader rwHeader; + NTAGAreaHeader roHeader; + + readResult->readOnly = readOnly; + + error = __NTAGConvertNFCError(error); + if (error == 0) + { + // Copy raw and locked data into a contigous buffer + memcpy(rawData, data, dataSize); + memcpy(rawData + dataSize, lockedData, lockedDataSize); + + error = __NTAGParseData(rawData, rwData.GetPointer(), roData.GetPointer(), uid, lockedDataSize, &noftHeader, &infoHeader, &rwHeader, &roHeader); + if (error == 0) + { + memcpy(readResult->uid.uid, uid->uid, sizeof(uid->uid)); + readResult->rwInfo.data = _swapEndianU32(rwData.GetMPTR()); + readResult->roInfo.data = _swapEndianU32(roData.GetMPTR()); + readResult->rwInfo.makerCode = rwHeader.makerCode; + readResult->rwInfo.size = rwHeader.size; + readResult->roInfo.makerCode = roHeader.makerCode; + readResult->rwInfo.identifyCode = rwHeader.identifyCode; + readResult->roInfo.identifyCode = roHeader.identifyCode; + readResult->formatVersion = infoHeader.formatVersion; + readResult->roInfo.size = roHeader.size; + + cemuLog_log(LogType::NTAG, "__NTAGReadCallback: {} {} {}", chan, error, context); + + PPCCoreCallback(gReadCallbacks[chan], chan, 0, readResult.GetPointer(), context); + osLib_returnFromFunction(hCPU, 0); + return; + } + } + + if (uid) + { + memcpy(readResult->uid.uid, uid->uid, sizeof(uid->uid)); + } + readResult->roInfo.size = 0; + readResult->rwInfo.size = 0; + readResult->roInfo.data = MPTR_NULL; + readResult->formatVersion = 0; + readResult->rwInfo.data = MPTR_NULL; + cemuLog_log(LogType::NTAG, "__NTAGReadCallback: {} {} {}", chan, error, context); + PPCCoreCallback(gReadCallbacks[chan], chan, error, readResult.GetPointer(), context); + osLib_returnFromFunction(hCPU, 0); + } + + sint32 NTAGRead(uint32 chan, uint32 timeout, nfc::NFCUid* uid, nfc::NFCUid* uidMask, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + gReadCallbacks[chan] = callback; + + nfc::NFCUid _uid{}, _uidMask{}; + if (uid && uidMask) + { + memcpy(&_uid, uid, sizeof(*uid)); + memcpy(&_uidMask, uidMask, sizeof(*uidMask)); + } + + sint32 result = nfc::NFCRead(chan, timeout, &_uid, &_uidMask, RPLLoader_MakePPCCallable(__NTAGReadCallback), context); + return __NTAGConvertNFCError(result); + } + + void __NTAGReadBeforeWriteCallback(PPCInterpreter_t* hCPU) + { + osLib_returnFromFunction(hCPU, 0); + } + + sint32 NTAGWrite(uint32 chan, uint32 timeout, nfc::NFCUid* uid, uint32 rwSize, void* rwData, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + gWriteCallbacks[chan] = callback; + + nfc::NFCUid _uid{}, _uidMask{}; + if (uid) + { + memcpy(&_uid, uid, sizeof(*uid)); + } + memset(_uidMask.uid, 0xff, sizeof(_uidMask.uid)); + + // TODO save write data + + // TODO we probably don't need to read first here + sint32 result = nfc::NFCRead(chan, timeout, &_uid, &_uidMask, RPLLoader_MakePPCCallable(__NTAGReadBeforeWriteCallback), context); + return __NTAGConvertNFCError(result); + } + + sint32 NTAGFormat(uint32 chan, uint32 timeout, nfc::NFCUid* uid, uint32 rwSize, void* rwData, MPTR callback, void* context) + { + cemu_assert(chan < 2); + + // TODO + return 0; + } + + void Initialize() + { + cafeExportRegister("ntag", NTAGInit, LogType::NTAG); + cafeExportRegister("ntag", NTAGInitEx, LogType::NTAG); + cafeExportRegister("ntag", NTAGShutdown, LogType::NTAG); + cafeExportRegister("ntag", NTAGIsInit, LogType::Placeholder); // disabled logging, since this gets spammed + cafeExportRegister("ntag", NTAGProc, LogType::Placeholder); // disabled logging, since this gets spammed + cafeExportRegister("ntag", NTAGSetFormatSettings, LogType::NTAG); + cafeExportRegister("ntag", NTAGSetTagDetectCallback, LogType::NTAG); + cafeExportRegister("ntag", NTAGAbort, LogType::NTAG); + cafeExportRegister("ntag", NTAGRead, LogType::NTAG); + cafeExportRegister("ntag", NTAGWrite, LogType::NTAG); + cafeExportRegister("ntag", NTAGFormat, LogType::NTAG); + } +} diff --git a/src/Cafe/OS/libs/ntag/ntag.h b/src/Cafe/OS/libs/ntag/ntag.h new file mode 100644 index 00000000..1174e6bc --- /dev/null +++ b/src/Cafe/OS/libs/ntag/ntag.h @@ -0,0 +1,94 @@ +#pragma once +#include "Cafe/OS/libs/nfc/nfc.h" + +namespace ntag +{ + struct NTAGFormatSettings + { + /* +0x00 */ uint8 version; + /* +0x04 */ uint32 makerCode; + /* +0x08 */ uint32 indentifyCode; + /* +0x0C */ uint8 reserved[0x1C]; + }; + static_assert(sizeof(NTAGFormatSettings) == 0x28); + +#pragma pack(1) + struct NTAGNoftHeader + { + /* +0x00 */ uint32 magic; + /* +0x04 */ uint8 version; + /* +0x05 */ uint16 writeCount; + /* +0x07 */ uint8 unknown; + }; + static_assert(sizeof(NTAGNoftHeader) == 0x8); +#pragma pack() + + struct NTAGInfoHeader + { + /* +0x00 */ uint16 rwHeaderOffset; + /* +0x02 */ uint16 rwSize; + /* +0x04 */ uint16 roHeaderOffset; + /* +0x06 */ uint16 roSize; + /* +0x08 */ nfc::NFCUid uid; + /* +0x0F */ uint8 formatVersion; + }; + static_assert(sizeof(NTAGInfoHeader) == 0x10); + + struct NTAGAreaHeader + { + /* +0x00 */ uint16 magic; + /* +0x02 */ uint16 offset; + /* +0x04 */ uint16 size; + /* +0x06 */ uint16 padding; + /* +0x08 */ uint32 makerCode; + /* +0x0C */ uint32 identifyCode; + }; + static_assert(sizeof(NTAGAreaHeader) == 0x10); + + struct NTAGAreaInfo + { + /* +0x00 */ MPTR data; + /* +0x04 */ uint16 size; + /* +0x06 */ uint16 padding; + /* +0x08 */ uint32 makerCode; + /* +0x0C */ uint32 identifyCode; + /* +0x10 */ uint8 reserved[0x20]; + }; + static_assert(sizeof(NTAGAreaInfo) == 0x30); + + struct NTAGData + { + /* +0x00 */ nfc::NFCUid uid; + /* +0x07 */ uint8 readOnly; + /* +0x08 */ uint8 formatVersion; + /* +0x09 */ uint8 padding[3]; + /* +0x0C */ NTAGAreaInfo rwInfo; + /* +0x3C */ NTAGAreaInfo roInfo; + /* +0x6C */ uint8 reserved[0x20]; + }; + static_assert(sizeof(NTAGData) == 0x8C); + + sint32 NTAGInit(uint32 chan); + + sint32 NTAGInitEx(uint32 chan); + + sint32 NTAGShutdown(uint32 chan); + + bool NTAGIsInit(uint32 chan); + + void NTAGProc(uint32 chan); + + void NTAGSetFormatSettings(NTAGFormatSettings* formatSettings); + + void NTAGSetTagDetectCallback(uint32 chan, MPTR callback, void* context); + + sint32 NTAGAbort(uint32 chan, MPTR callback, void* context); + + sint32 NTAGRead(uint32 chan, uint32 timeout, nfc::NFCUid* uid, nfc::NFCUid* uidMask, MPTR callback, void* context); + + sint32 NTAGWrite(uint32 chan, uint32 timeout, nfc::NFCUid* uid, uint32 rwSize, void* rwData, MPTR callback, void* context); + + sint32 NTAGFormat(uint32 chan, uint32 timeout, nfc::NFCUid* uid, uint32 rwSize, void* rwData, MPTR callback, void* context); + + void Initialize(); +} diff --git a/src/Cemu/Logging/CemuLogging.cpp b/src/Cemu/Logging/CemuLogging.cpp index 058ab07a..e49ece94 100644 --- a/src/Cemu/Logging/CemuLogging.cpp +++ b/src/Cemu/Logging/CemuLogging.cpp @@ -51,6 +51,8 @@ const std::map g_logging_window_mapping {LogType::Socket, "Socket"}, {LogType::Save, "Save"}, {LogType::H264, "H264"}, + {LogType::NFC, "NFC"}, + {LogType::NTAG, "NTAG"}, {LogType::Patches, "Graphic pack patches"}, {LogType::TextureCache, "Texture cache"}, {LogType::TextureReadback, "Texture readback"}, diff --git a/src/Cemu/Logging/CemuLogging.h b/src/Cemu/Logging/CemuLogging.h index 8fbb318c..5fd652b3 100644 --- a/src/Cemu/Logging/CemuLogging.h +++ b/src/Cemu/Logging/CemuLogging.h @@ -44,6 +44,9 @@ enum class LogType : sint32 nlibcurl = 41, PRUDP = 40, + + NFC = 41, + NTAG = 42, }; template <> diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 097d506e..cb2e988d 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -12,7 +12,7 @@ #include "audio/audioDebuggerWindow.h" #include "gui/canvas/OpenGLCanvas.h" #include "gui/canvas/VulkanCanvas.h" -#include "Cafe/OS/libs/nn_nfp/nn_nfp.h" +#include "Cafe/OS/libs/nfc/nfc.h" #include "Cafe/OS/libs/swkbd/swkbd.h" #include "gui/debugger/DebuggerWindow2.h" #include "util/helpers/helpers.h" @@ -261,7 +261,7 @@ public: return false; uint32 nfcError; std::string path = filenames[0].utf8_string(); - if (nnNfp_touchNfcTagFromFile(_utf8ToPath(path), &nfcError)) + if (nfc::TouchTagFromFile(_utf8ToPath(path), &nfcError)) { GetConfig().AddRecentNfcFile(path); m_window->UpdateNFCMenu(); @@ -749,7 +749,7 @@ void MainWindow::OnNFCMenu(wxCommandEvent& event) return; wxString wxStrFilePath = openFileDialog.GetPath(); uint32 nfcError; - if (nnNfp_touchNfcTagFromFile(_utf8ToPath(wxStrFilePath.utf8_string()), &nfcError) == false) + if (nfc::TouchTagFromFile(_utf8ToPath(wxStrFilePath.utf8_string()), &nfcError) == false) { if (nfcError == NFC_ERROR_NO_ACCESS) wxMessageBox(_("Cannot open file")); @@ -772,7 +772,7 @@ void MainWindow::OnNFCMenu(wxCommandEvent& event) if (!path.empty()) { uint32 nfcError = 0; - if (nnNfp_touchNfcTagFromFile(_utf8ToPath(path), &nfcError) == false) + if (nfc::TouchTagFromFile(_utf8ToPath(path), &nfcError) == false) { if (nfcError == NFC_ERROR_NO_ACCESS) wxMessageBox(_("Cannot open file")); @@ -2210,6 +2210,8 @@ void MainWindow::RecreateMenu() debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Socket), _("&Socket API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Socket)); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Save), _("&Save API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Save)); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::H264), _("&H264 API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::H264)); + debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NFC), _("&NFC API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NFC)); + debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NTAG), _("&NTAG API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NTAG)); debugLoggingMenu->AppendSeparator(); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Patches), _("&Graphic pack patches"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Patches)); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::TextureCache), _("&Texture cache warnings"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::TextureCache)); From 8e8431113a4128330351674ca59771cf203bf8d9 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Fri, 10 May 2024 00:33:31 +0200 Subject: [PATCH 04/73] ntag: Implement NTAGWrite --- src/Cafe/OS/libs/nfc/ndef.cpp | 1 + src/Cafe/OS/libs/nfc/nfc.cpp | 119 +++++++++++++----- src/Cafe/OS/libs/ntag/ntag.cpp | 214 +++++++++++++++++++++++++++++++-- src/Cafe/OS/libs/ntag/ntag.h | 2 +- 4 files changed, 293 insertions(+), 43 deletions(-) diff --git a/src/Cafe/OS/libs/nfc/ndef.cpp b/src/Cafe/OS/libs/nfc/ndef.cpp index f8d87fb8..32097cfd 100644 --- a/src/Cafe/OS/libs/nfc/ndef.cpp +++ b/src/Cafe/OS/libs/nfc/ndef.cpp @@ -6,6 +6,7 @@ namespace ndef { Record::Record() + : mFlags(0), mTNF(NDEF_TNF_EMPTY) { } diff --git a/src/Cafe/OS/libs/nfc/nfc.cpp b/src/Cafe/OS/libs/nfc/nfc.cpp index 21e9e91b..f8f67ebd 100644 --- a/src/Cafe/OS/libs/nfc/nfc.cpp +++ b/src/Cafe/OS/libs/nfc/nfc.cpp @@ -39,6 +39,7 @@ namespace nfc bool hasTag; uint32 nfcStatus; + std::chrono::time_point touchTime; std::chrono::time_point discoveryTimeout; MPTR tagDetectCallback; @@ -146,7 +147,8 @@ namespace nfc // Look for the unknown TNF which contains the data we care about for (const auto& rec : *ndefMsg) { - if (rec.GetTNF() == ndef::Record::NDEF_TNF_UNKNOWN) { + if (rec.GetTNF() == ndef::Record::NDEF_TNF_UNKNOWN) + { dataSize = rec.GetPayload().size(); cemu_assert(dataSize < 0x200); memcpy(data.GetPointer(), rec.GetPayload().data(), dataSize); @@ -174,11 +176,6 @@ namespace nfc { result = -0xBFE; } - - // Clear tag status after read - // TODO this is not really nice here - ctx->nfcStatus &= ~NFC_STATUS_HAS_TAG; - ctx->tag = {}; } else { @@ -194,9 +191,42 @@ namespace nfc ctx->state = NFC_STATE_IDLE; - // TODO write to file + sint32 result; - PPCCoreCallback(ctx->writeCallback, chan, 0, ctx->writeContext); + if (ctx->tag) + { + // Update tag NDEF data + ctx->tag->SetNDEFData(ctx->writeMessage.ToBytes()); + + // TODO remove this once writing is confirmed working + fs::path newPath = ctx->tagPath; + if (newPath.extension() != ".bak") + { + newPath += ".bak"; + } + cemuLog_log(LogType::Force, "Saving tag as {}...", newPath.string()); + + // open file for writing + FileStream* fs = FileStream::createFile2(newPath); + if (!fs) + { + result = -0x2DE; + } + else + { + auto tagBytes = ctx->tag->ToBytes(); + fs->writeData(tagBytes.data(), tagBytes.size()); + delete fs; + + result = 0; + } + } + else + { + result = -0x2DD; + } + + PPCCoreCallback(ctx->writeCallback, chan, result, ctx->writeContext); } void __NFCHandleAbort(uint32 chan) @@ -231,6 +261,29 @@ namespace nfc PPCCoreCallback(ctx->rawCallback, chan, result, responseSize, responseData, ctx->rawContext); } + bool __NFCShouldHandleState(NFCContext* ctx) + { + // Always handle abort + if (ctx->state == NFC_STATE_ABORT) + { + return true; + } + + // Do we have a tag? + if (ctx->nfcStatus & NFC_STATUS_HAS_TAG) + { + return true; + } + + // Did the timeout expire? + if (ctx->discoveryTimeout < std::chrono::system_clock::now()) + { + return true; + } + + return false; + } + void NFCProc(uint32 chan) { cemu_assert(chan < 2); @@ -242,6 +295,11 @@ namespace nfc return; } + if (ctx->state == NFC_STATE_INITIALIZED) + { + ctx->state = NFC_STATE_IDLE; + } + // Check if the detect callback should be called if (ctx->nfcStatus & NFC_STATUS_HAS_TAG) { @@ -254,6 +312,14 @@ namespace nfc ctx->hasTag = true; } + + // Check if the tag should be removed again + if (ctx->touchTime + std::chrono::seconds(2) < std::chrono::system_clock::now()) + { + ctx->nfcStatus &= ~NFC_STATUS_HAS_TAG; + ctx->tag = {}; + ctx->tagPath = ""; + } } else { @@ -268,33 +334,25 @@ namespace nfc } } - switch (ctx->state) + if (__NFCShouldHandleState(ctx)) { - case NFC_STATE_INITIALIZED: - ctx->state = NFC_STATE_IDLE; - break; - case NFC_STATE_IDLE: - break; - case NFC_STATE_READ: - // Do we have a tag or did the timeout expire? - if ((ctx->nfcStatus & NFC_STATUS_HAS_TAG) || ctx->discoveryTimeout < std::chrono::system_clock::now()) + switch (ctx->state) { + case NFC_STATE_READ: __NFCHandleRead(chan); - } - break; - case NFC_STATE_WRITE: - __NFCHandleWrite(chan); - break; - case NFC_STATE_ABORT: - __NFCHandleAbort(chan); - break; - case NFC_STATE_RAW: - // Do we have a tag or did the timeout expire? - if ((ctx->nfcStatus & NFC_STATUS_HAS_TAG) || ctx->discoveryTimeout < std::chrono::system_clock::now()) - { + break; + case NFC_STATE_WRITE: + __NFCHandleWrite(chan); + break; + case NFC_STATE_ABORT: + __NFCHandleAbort(chan); + break; + case NFC_STATE_RAW: __NFCHandleRaw(chan); + break; + default: + break; } - break; } } @@ -589,6 +647,7 @@ namespace nfc ctx->nfcStatus |= NFC_STATUS_HAS_TAG; ctx->tagPath = filePath; + ctx->touchTime = std::chrono::system_clock::now(); *nfcError = NFC_ERROR_NONE; return true; diff --git a/src/Cafe/OS/libs/ntag/ntag.cpp b/src/Cafe/OS/libs/ntag/ntag.cpp index 8bdbb66f..18ed798a 100644 --- a/src/Cafe/OS/libs/ntag/ntag.cpp +++ b/src/Cafe/OS/libs/ntag/ntag.cpp @@ -9,7 +9,10 @@ namespace ntag { struct NTAGWriteData { - + uint16 size; + uint8 data[0x1C8]; + nfc::NFCUid uid; + nfc::NFCUid uidMask; }; NTAGWriteData gWriteData[2]; @@ -72,7 +75,7 @@ namespace ntag { gFormatSettings.version = formatSettings->version; gFormatSettings.makerCode = _swapEndianU32(formatSettings->makerCode); - gFormatSettings.indentifyCode = _swapEndianU32(formatSettings->indentifyCode); + gFormatSettings.identifyCode = _swapEndianU32(formatSettings->identifyCode); } void __NTAGDetectCallback(PPCInterpreter_t* hCPU) @@ -220,7 +223,7 @@ namespace ntag return true; } - sint32 __NTAGDecryptData(void* decryptedData, void* rawData) + sint32 __NTAGDecryptData(void* decryptedData, const void* rawData) { StackAllocator nfcRawData, nfcInData, nfcOutData; @@ -256,7 +259,41 @@ namespace ntag sint32 __NTAGValidateHeaders(NTAGNoftHeader* noftHeader, NTAGInfoHeader* infoHeader, NTAGAreaHeader* rwHeader, NTAGAreaHeader* roHeader) { - // TODO + if (infoHeader->formatVersion != gFormatSettings.version || noftHeader->version != 0x1) + { + cemuLog_log(LogType::Force, "Invalid format version"); + return -0x2710; + } + + if (_swapEndianU32(noftHeader->magic) != 0x4E4F4654 /* 'NOFT' */ || + _swapEndianU16(rwHeader->magic) != 0x5257 /* 'RW' */ || + _swapEndianU16(roHeader->magic) != 0x524F /* 'RO' */) + { + cemuLog_log(LogType::Force, "Invalid header magic"); + return -0x270F; + } + + if (_swapEndianU32(rwHeader->makerCode) != gFormatSettings.makerCode || + _swapEndianU32(roHeader->makerCode) != gFormatSettings.makerCode) + { + cemuLog_log(LogType::Force, "Invalid maker code"); + return -0x270E; + } + + if (infoHeader->formatVersion != 0 && + (_swapEndianU32(rwHeader->identifyCode) != gFormatSettings.identifyCode || + _swapEndianU32(roHeader->identifyCode) != gFormatSettings.identifyCode)) + { + cemuLog_log(LogType::Force, "Invalid identify code"); + return -0x2709; + } + + if (_swapEndianU16(rwHeader->size) + _swapEndianU16(roHeader->size) != 0x130) + { + cemuLog_log(LogType::Force, "Invalid data size"); + return -0x270D; + } + return 0; } @@ -264,8 +301,13 @@ namespace ntag { memcpy(noftHeader, data + 0x20, sizeof(NTAGNoftHeader)); memcpy(infoHeader, data + 0x198, sizeof(NTAGInfoHeader)); + + cemu_assert(_swapEndianU16(infoHeader->rwHeaderOffset) + sizeof(NTAGAreaHeader) < 0x200); + cemu_assert(_swapEndianU16(infoHeader->roHeaderOffset) + sizeof(NTAGAreaHeader) < 0x200); + memcpy(rwHeader, data + _swapEndianU16(infoHeader->rwHeaderOffset), sizeof(NTAGAreaHeader)); memcpy(roHeader, data + _swapEndianU16(infoHeader->roHeaderOffset), sizeof(NTAGAreaHeader)); + return __NTAGValidateHeaders(noftHeader, infoHeader, rwHeader, roHeader); } @@ -317,7 +359,7 @@ namespace ntag ppcDefineParamPtr(lockedData, void, 7); ppcDefineParamPtr(context, void, 8); - uint8 rawData[0x1C8]; + uint8 rawData[0x1C8]{}; StackAllocator readResult; StackAllocator rwData; StackAllocator roData; @@ -331,6 +373,9 @@ namespace ntag error = __NTAGConvertNFCError(error); if (error == 0) { + memset(rwData.GetPointer(), 0, 0x1C8); + memset(roData.GetPointer(), 0, 0x1C8); + // Copy raw and locked data into a contigous buffer memcpy(rawData, data, dataSize); memcpy(rawData + dataSize, lockedData, lockedDataSize); @@ -388,28 +433,173 @@ namespace ntag return __NTAGConvertNFCError(result); } + sint32 __NTAGEncryptData(void* encryptedData, const void* rawData) + { + StackAllocator nfcRawData, nfcInData, nfcOutData; + + if (!ccrNfcOpened) + { + gCcrNfcHandle = coreinit::IOS_Open("/dev/ccr_nfc", 0); + } + + // Prepare nfc buffer + nfcRawData->version = 0; + memcpy(nfcRawData->data, rawData, 0x1C8); + __NTAGRawDataToNfcData(nfcRawData.GetPointer(), nfcInData.GetPointer()); + + // Encrypt + sint32 result = coreinit::IOS_Ioctl(gCcrNfcHandle, 1, nfcInData.GetPointer(), sizeof(iosu::ccr_nfc::CCRNFCCryptData), nfcOutData.GetPointer(), sizeof(iosu::ccr_nfc::CCRNFCCryptData)); + + // Unpack nfc buffer + __NTAGNfcDataToRawData(nfcOutData.GetPointer(), nfcRawData.GetPointer()); + memcpy(encryptedData, nfcRawData->data, 0x1C8); + + return result; + } + + sint32 __NTAGPrepareWriteData(void* outBuffer, uint32 dataSize, const void* data, const void* tagData, NTAGNoftHeader* noftHeader, NTAGAreaHeader* rwHeader) + { + uint8 decryptedBuffer[0x1C8]; + uint8 encryptedBuffer[0x1C8]; + + memcpy(decryptedBuffer, tagData, 0x1C8); + + // Fill the rest of the rw area with random data + if (dataSize < _swapEndianU16(rwHeader->size)) + { + uint8 randomBuffer[0x1C8]; + for (int i = 0; i < sizeof(randomBuffer); i++) + { + randomBuffer[i] = rand() & 0xFF; + } + + memcpy(decryptedBuffer + _swapEndianU16(rwHeader->offset) + dataSize, randomBuffer, _swapEndianU16(rwHeader->size) - dataSize); + } + + // Make sure the data fits into the rw area + if (_swapEndianU16(rwHeader->size) < dataSize) + { + return -0x270D; + } + + // Update write count (check for overflow) + if ((_swapEndianU16(noftHeader->writeCount) & 0x7fff) == 0x7fff) + { + noftHeader->writeCount = _swapEndianU16(_swapEndianU16(noftHeader->writeCount) & 0x8000); + } + else + { + noftHeader->writeCount = _swapEndianU16(_swapEndianU16(noftHeader->writeCount) + 1); + } + + memcpy(decryptedBuffer + 0x20, noftHeader, sizeof(noftHeader)); + memcpy(decryptedBuffer + _swapEndianU16(rwHeader->offset), data, dataSize); + + // Encrypt + sint32 result = __NTAGEncryptData(encryptedBuffer, decryptedBuffer); + if (result < 0) + { + return result; + } + + memcpy(outBuffer, encryptedBuffer, _swapEndianU16(rwHeader->size) + 0x28); + return 0; + } + + void __NTAGWriteCallback(PPCInterpreter_t* hCPU) + { + ppcDefineParamU32(chan, 0); + ppcDefineParamS32(error, 1); + ppcDefineParamPtr(context, void, 2); + + PPCCoreCallback(gWriteCallbacks[chan], chan, __NTAGConvertNFCError(error), context); + + osLib_returnFromFunction(hCPU, 0); + } + void __NTAGReadBeforeWriteCallback(PPCInterpreter_t* hCPU) { + ppcDefineParamU32(chan, 0); + ppcDefineParamS32(error, 1); + ppcDefineParamPtr(uid, nfc::NFCUid, 2); + ppcDefineParamU32(readOnly, 3); + ppcDefineParamU32(dataSize, 4); + ppcDefineParamPtr(data, void, 5); + ppcDefineParamU32(lockedDataSize, 6); + ppcDefineParamPtr(lockedData, void, 7); + ppcDefineParamPtr(context, void, 8); + + uint8 rawData[0x1C8]{}; + uint8 rwData[0x1C8]{}; + uint8 roData[0x1C8]{}; + NTAGNoftHeader noftHeader; + NTAGInfoHeader infoHeader; + NTAGAreaHeader rwHeader; + NTAGAreaHeader roHeader; + uint8 writeBuffer[0x1C8]{}; + + error = __NTAGConvertNFCError(error); + if (error == 0) + { + // Copy raw and locked data into a contigous buffer + memcpy(rawData, data, dataSize); + memcpy(rawData + dataSize, lockedData, lockedDataSize); + + error = __NTAGParseData(rawData, rwData, roData, uid, lockedDataSize, &noftHeader, &infoHeader, &rwHeader, &roHeader); + if (error < 0) + { + cemuLog_log(LogType::Force, "Failed to parse data before write"); + PPCCoreCallback(gWriteCallbacks[chan], chan, -0x3E3, context); + osLib_returnFromFunction(hCPU, 0); + return; + } + + // Prepare data + memcpy(rawData + _swapEndianU16(infoHeader.rwHeaderOffset), &rwHeader, sizeof(rwHeader)); + memcpy(rawData + _swapEndianU16(infoHeader.roHeaderOffset), &roHeader, sizeof(roHeader)); + memcpy(rawData + _swapEndianU16(roHeader.offset), roData, _swapEndianU16(roHeader.size)); + error = __NTAGPrepareWriteData(writeBuffer, gWriteData[chan].size, gWriteData[chan].data, rawData, &noftHeader, &rwHeader); + if (error < 0) + { + cemuLog_log(LogType::Force, "Failed to prepare write data"); + PPCCoreCallback(gWriteCallbacks[chan], chan, -0x3E3, context); + osLib_returnFromFunction(hCPU, 0); + return; + } + + // Write data to tag + error = nfc::NFCWrite(chan, 200, &gWriteData[chan].uid, &gWriteData[chan].uidMask, + _swapEndianU16(rwHeader.size) + 0x28, writeBuffer, RPLLoader_MakePPCCallable(__NTAGWriteCallback), context); + if (error >= 0) + { + osLib_returnFromFunction(hCPU, 0); + return; + } + + error = __NTAGConvertNFCError(error); + } + + PPCCoreCallback(gWriteCallbacks[chan], chan, error, context); osLib_returnFromFunction(hCPU, 0); } sint32 NTAGWrite(uint32 chan, uint32 timeout, nfc::NFCUid* uid, uint32 rwSize, void* rwData, MPTR callback, void* context) { cemu_assert(chan < 2); + cemu_assert(rwSize < 0x1C8); gWriteCallbacks[chan] = callback; - nfc::NFCUid _uid{}, _uidMask{}; if (uid) { - memcpy(&_uid, uid, sizeof(*uid)); + memcpy(&gWriteData[chan].uid, uid, sizeof(nfc::NFCUid)); } - memset(_uidMask.uid, 0xff, sizeof(_uidMask.uid)); + memset(&gWriteData[chan].uidMask, 0xff, sizeof(nfc::NFCUid)); - // TODO save write data + gWriteData[chan].size = rwSize; + memcpy(gWriteData[chan].data, rwData, rwSize); - // TODO we probably don't need to read first here - sint32 result = nfc::NFCRead(chan, timeout, &_uid, &_uidMask, RPLLoader_MakePPCCallable(__NTAGReadBeforeWriteCallback), context); + sint32 result = nfc::NFCRead(chan, timeout, &gWriteData[chan].uid, &gWriteData[chan].uidMask, RPLLoader_MakePPCCallable(__NTAGReadBeforeWriteCallback), context); return __NTAGConvertNFCError(result); } @@ -418,7 +608,7 @@ namespace ntag cemu_assert(chan < 2); // TODO - return 0; + return -1; } void Initialize() diff --git a/src/Cafe/OS/libs/ntag/ntag.h b/src/Cafe/OS/libs/ntag/ntag.h index 1174e6bc..697c065e 100644 --- a/src/Cafe/OS/libs/ntag/ntag.h +++ b/src/Cafe/OS/libs/ntag/ntag.h @@ -7,7 +7,7 @@ namespace ntag { /* +0x00 */ uint8 version; /* +0x04 */ uint32 makerCode; - /* +0x08 */ uint32 indentifyCode; + /* +0x08 */ uint32 identifyCode; /* +0x0C */ uint8 reserved[0x1C]; }; static_assert(sizeof(NTAGFormatSettings) == 0x28); From 41fe598e333920196aa8fd6033aaa78172e21655 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Fri, 17 May 2024 14:19:51 +0200 Subject: [PATCH 05/73] nfc: Implement UID filter --- src/Cafe/OS/libs/nfc/nfc.cpp | 118 ++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 42 deletions(-) diff --git a/src/Cafe/OS/libs/nfc/nfc.cpp b/src/Cafe/OS/libs/nfc/nfc.cpp index f8f67ebd..4505f3b1 100644 --- a/src/Cafe/OS/libs/nfc/nfc.cpp +++ b/src/Cafe/OS/libs/nfc/nfc.cpp @@ -41,6 +41,10 @@ namespace nfc uint32 nfcStatus; std::chrono::time_point touchTime; std::chrono::time_point discoveryTimeout; + struct { + NFCUid uid; + NFCUid mask; + } filter; MPTR tagDetectCallback; void* tagDetectContext; @@ -124,6 +128,19 @@ namespace nfc return gNFCContexts[chan].isInitialized; } + bool __NFCCompareUid(NFCUid* uid, NFCUid* filterUid, NFCUid* filterMask) + { + for (int i = 0; i < sizeof(uid->uid); i++) + { + if ((uid->uid[i] & filterMask->uid[i]) != filterUid->uid[i]) + { + return false; + } + } + + return true; + } + void __NFCHandleRead(uint32 chan) { NFCContext* ctx = &gNFCContexts[chan]; @@ -140,32 +157,38 @@ namespace nfc if (ctx->tag) { - // Try to parse ndef message - auto ndefMsg = ndef::Message::FromBytes(ctx->tag->GetNDEFData()); - if (ndefMsg) + // Compare UID + memcpy(uid.GetPointer(), ctx->tag->GetUIDBlock().data(), sizeof(NFCUid)); + if (__NFCCompareUid(uid.GetPointer(), &ctx->filter.uid, &ctx->filter.mask)) { - // Look for the unknown TNF which contains the data we care about - for (const auto& rec : *ndefMsg) + // Try to parse ndef message + auto ndefMsg = ndef::Message::FromBytes(ctx->tag->GetNDEFData()); + if (ndefMsg) { - if (rec.GetTNF() == ndef::Record::NDEF_TNF_UNKNOWN) + // Look for the unknown TNF which contains the data we care about + for (const auto& rec : *ndefMsg) { - dataSize = rec.GetPayload().size(); - cemu_assert(dataSize < 0x200); - memcpy(data.GetPointer(), rec.GetPayload().data(), dataSize); - break; + if (rec.GetTNF() == ndef::Record::NDEF_TNF_UNKNOWN) + { + dataSize = rec.GetPayload().size(); + cemu_assert(dataSize < 0x200); + memcpy(data.GetPointer(), rec.GetPayload().data(), dataSize); + break; + } } - } - if (dataSize) - { - // Get locked data - lockedDataSize = ctx->tag->GetLockedArea().size(); - memcpy(lockedData.GetPointer(), ctx->tag->GetLockedArea().data(), lockedDataSize); + if (dataSize) + { + // Get locked data + lockedDataSize = ctx->tag->GetLockedArea().size(); + memcpy(lockedData.GetPointer(), ctx->tag->GetLockedArea().data(), lockedDataSize); - // Fill in uid - memcpy(uid.GetPointer(), ctx->tag->GetUIDBlock().data(), sizeof(NFCUid)); - - result = 0; + result = 0; + } + else + { + result = -0xBFE; + } } else { @@ -174,7 +197,7 @@ namespace nfc } else { - result = -0xBFE; + result = -0x1F6; } } else @@ -195,30 +218,39 @@ namespace nfc if (ctx->tag) { - // Update tag NDEF data - ctx->tag->SetNDEFData(ctx->writeMessage.ToBytes()); - - // TODO remove this once writing is confirmed working - fs::path newPath = ctx->tagPath; - if (newPath.extension() != ".bak") + NFCUid uid; + memcpy(&uid, ctx->tag->GetUIDBlock().data(), sizeof(NFCUid)); + if (__NFCCompareUid(&uid, &ctx->filter.uid, &ctx->filter.mask)) { - newPath += ".bak"; - } - cemuLog_log(LogType::Force, "Saving tag as {}...", newPath.string()); + // Update tag NDEF data + ctx->tag->SetNDEFData(ctx->writeMessage.ToBytes()); - // open file for writing - FileStream* fs = FileStream::createFile2(newPath); - if (!fs) - { - result = -0x2DE; + // TODO remove this once writing is confirmed working + fs::path newPath = ctx->tagPath; + if (newPath.extension() != ".bak") + { + newPath += ".bak"; + } + cemuLog_log(LogType::Force, "Saving tag as {}...", newPath.string()); + + // open file for writing + FileStream* fs = FileStream::createFile2(newPath); + if (!fs) + { + result = -0x2DE; + } + else + { + auto tagBytes = ctx->tag->ToBytes(); + fs->writeData(tagBytes.data(), tagBytes.size()); + delete fs; + + result = 0; + } } else { - auto tagBytes = ctx->tag->ToBytes(); - fs->writeData(tagBytes.data(), tagBytes.size()); - delete fs; - - result = 0; + result = -0x2F6; } } else @@ -548,7 +580,8 @@ namespace nfc ctx->discoveryTimeout = std::chrono::system_clock::now() + std::chrono::milliseconds(discoveryTimeout); } - // TODO uid filter? + memcpy(&ctx->filter.uid, uid, sizeof(*uid)); + memcpy(&ctx->filter.mask, uidMask, sizeof(*uidMask)); return 0; } @@ -598,7 +631,8 @@ namespace nfc ctx->discoveryTimeout = std::chrono::system_clock::now() + std::chrono::milliseconds(discoveryTimeout); } - // TODO uid filter? + memcpy(&ctx->filter.uid, uid, sizeof(*uid)); + memcpy(&ctx->filter.mask, uidMask, sizeof(*uidMask)); return 0; } From 8fe69cd0fb6be8d916a963290e7c5525c0848bb5 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Sat, 18 May 2024 16:38:52 +0200 Subject: [PATCH 06/73] Properly implement NFC result codes --- src/Cafe/OS/libs/nfc/nfc.cpp | 133 ++++++++++++++++++--------------- src/Cafe/OS/libs/nfc/nfc.h | 36 ++++++++- src/Cafe/OS/libs/ntag/ntag.cpp | 47 ++++++++---- src/Cafe/OS/libs/ntag/ntag.h | 7 ++ src/gui/MainWindow.cpp | 18 ++--- 5 files changed, 153 insertions(+), 88 deletions(-) diff --git a/src/Cafe/OS/libs/nfc/nfc.cpp b/src/Cafe/OS/libs/nfc/nfc.cpp index 4505f3b1..818c7339 100644 --- a/src/Cafe/OS/libs/nfc/nfc.cpp +++ b/src/Cafe/OS/libs/nfc/nfc.cpp @@ -7,27 +7,25 @@ #include "TagV0.h" #include "ndef.h" -// TODO move errors to header and allow ntag to convert them +#define NFC_MODE_INVALID -1 +#define NFC_MODE_IDLE 0 +#define NFC_MODE_ACTIVE 1 -#define NFC_MODE_INVALID -1 -#define NFC_MODE_IDLE 0 -#define NFC_MODE_ACTIVE 1 +#define NFC_STATE_UNINITIALIZED 0x0 +#define NFC_STATE_INITIALIZED 0x1 +#define NFC_STATE_IDLE 0x2 +#define NFC_STATE_READ 0x3 +#define NFC_STATE_WRITE 0x4 +#define NFC_STATE_ABORT 0x5 +#define NFC_STATE_FORMAT 0x6 +#define NFC_STATE_SET_READ_ONLY 0x7 +#define NFC_STATE_TAG_PRESENT 0x8 +#define NFC_STATE_DETECT 0x9 +#define NFC_STATE_SEND_RAW_DATA 0xA -#define NFC_STATE_UNINITIALIZED 0x0 -#define NFC_STATE_INITIALIZED 0x1 -#define NFC_STATE_IDLE 0x2 -#define NFC_STATE_READ 0x3 -#define NFC_STATE_WRITE 0x4 -#define NFC_STATE_ABORT 0x5 -#define NFC_STATE_FORMAT 0x6 -#define NFC_STATE_SET_READ_ONLY 0x7 -#define NFC_STATE_TAG_PRESENT 0x8 -#define NFC_STATE_DETECT 0x9 -#define NFC_STATE_RAW 0xA - -#define NFC_STATUS_COMMAND_COMPLETE 0x1 -#define NFC_STATUS_READY 0x2 -#define NFC_STATUS_HAS_TAG 0x4 +#define NFC_STATUS_COMMAND_COMPLETE 0x1 +#define NFC_STATUS_READY 0x2 +#define NFC_STATUS_HAS_TAG 0x4 namespace nfc { @@ -107,7 +105,7 @@ namespace nfc ctx->isInitialized = true; ctx->state = NFC_STATE_INITIALIZED; - return 0; + return NFC_RESULT_SUCCESS; } sint32 NFCShutdown(uint32 chan) @@ -118,7 +116,7 @@ namespace nfc __NFCClearContext(ctx); - return 0; + return NFC_RESULT_SUCCESS; } bool NFCIsInit(uint32 chan) @@ -183,26 +181,26 @@ namespace nfc lockedDataSize = ctx->tag->GetLockedArea().size(); memcpy(lockedData.GetPointer(), ctx->tag->GetLockedArea().data(), lockedDataSize); - result = 0; + result = NFC_RESULT_SUCCESS; } else { - result = -0xBFE; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_TAG_PARSE, NFC_RESULT_INVALID_TAG); } } else { - result = -0xBFE; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_TAG_PARSE, NFC_RESULT_INVALID_TAG); } } else { - result = -0x1F6; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_READ, NFC_RESULT_UID_MISMATCH); } } else { - result = -0x1DD; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_READ, NFC_RESULT_NO_TAG); } PPCCoreCallback(ctx->readCallback, chan, result, uid.GetPointer(), readOnly, dataSize, data.GetPointer(), lockedDataSize, lockedData.GetPointer(), ctx->readContext); @@ -231,13 +229,13 @@ namespace nfc { newPath += ".bak"; } - cemuLog_log(LogType::Force, "Saving tag as {}...", newPath.string()); + cemuLog_log(LogType::NFC, "Saving tag as {}...", newPath.string()); // open file for writing FileStream* fs = FileStream::createFile2(newPath); if (!fs) { - result = -0x2DE; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, 0x22); } else { @@ -245,17 +243,17 @@ namespace nfc fs->writeData(tagBytes.data(), tagBytes.size()); delete fs; - result = 0; + result = NFC_RESULT_SUCCESS; } } else { - result = -0x2F6; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, NFC_RESULT_UID_MISMATCH); } } else { - result = -0x2DD; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, NFC_RESULT_NO_TAG); } PPCCoreCallback(ctx->writeCallback, chan, result, ctx->writeContext); @@ -279,11 +277,11 @@ namespace nfc sint32 result; if (ctx->nfcStatus & NFC_STATUS_HAS_TAG) { - result = 0; + result = NFC_RESULT_SUCCESS; } else { - result = -0x9DD; + result = NFC_MAKE_RESULT(NFC_RESULT_BASE_SEND_RAW_DATA, NFC_RESULT_NO_TAG); } // We don't actually send any commands/responses @@ -379,12 +377,15 @@ namespace nfc case NFC_STATE_ABORT: __NFCHandleAbort(chan); break; - case NFC_STATE_RAW: + case NFC_STATE_SEND_RAW_DATA: __NFCHandleRaw(chan); break; default: break; } + + // Return back to idle mode + ctx->mode = NFC_MODE_IDLE; } } @@ -410,17 +411,17 @@ namespace nfc if (!NFCIsInit(chan)) { - return -0xAE0; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_SET_MODE, NFC_RESULT_UNINITIALIZED); } if (ctx->state == NFC_STATE_UNINITIALIZED) { - return -0xADF; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_SET_MODE, NFC_RESULT_INVALID_STATE); } ctx->mode = mode; - return 0; + return NFC_RESULT_SUCCESS; } void NFCSetTagDetectCallback(uint32 chan, MPTR callback, void* context) @@ -440,19 +441,30 @@ namespace nfc if (!NFCIsInit(chan)) { - return -0x6E0; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_ABORT, NFC_RESULT_UNINITIALIZED); } if (ctx->state <= NFC_STATE_IDLE) { - return -0x6DF; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_ABORT, NFC_RESULT_INVALID_STATE); } ctx->state = NFC_STATE_ABORT; ctx->abortCallback = callback; ctx->abortContext = context; - return 0; + return NFC_RESULT_SUCCESS; + } + + sint32 __NFCConvertGetTagInfoResult(sint32 result) + { + if (result == NFC_MAKE_RESULT(NFC_RESULT_BASE_SEND_RAW_DATA, NFC_RESULT_NO_TAG)) + { + return NFC_MAKE_RESULT(NFC_RESULT_BASE_GET_TAG_INFO, NFC_RESULT_TAG_INFO_TIMEOUT); + } + + // TODO convert the rest of the results + return result; } void __NFCGetTagInfoCallback(PPCInterpreter_t* hCPU) @@ -465,8 +477,7 @@ namespace nfc NFCContext* ctx = &gNFCContexts[chan]; - // TODO convert error - error = error; + error = __NFCConvertGetTagInfoResult(error); if (error == 0 && ctx->tag) { // this is usually parsed from response data @@ -496,7 +507,7 @@ namespace nfc ctx->getTagInfoCallback = callback; sint32 result = NFCSendRawData(chan, true, discoveryTimeout, 1000U, 0, 0, nullptr, RPLLoader_MakePPCCallable(__NFCGetTagInfoCallback), context); - return result; // TODO convert result + return __NFCConvertGetTagInfoResult(result); } sint32 NFCSendRawData(uint32 chan, bool startDiscovery, uint32 discoveryTimeout, uint32 commandTimeout, uint32 commandSize, uint32 responseSize, void* commandData, MPTR callback, void* context) @@ -507,26 +518,26 @@ namespace nfc if (!NFCIsInit(chan)) { - return -0x9E0; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_SEND_RAW_DATA, NFC_RESULT_UNINITIALIZED); } // Only allow discovery if (!startDiscovery) { - return -0x9DC; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_SEND_RAW_DATA, NFC_RESULT_INVALID_MODE); } if (NFCGetMode(chan) == NFC_MODE_ACTIVE && NFCSetMode(chan, NFC_MODE_IDLE) < 0) { - return -0x9DC; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_SEND_RAW_DATA, NFC_RESULT_INVALID_MODE); } if (ctx->state != NFC_STATE_IDLE) { - return -0x9DF; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_SEND_RAW_DATA, NFC_RESULT_INVALID_STATE); } - ctx->state = NFC_STATE_RAW; + ctx->state = NFC_STATE_SEND_RAW_DATA; ctx->rawCallback = callback; ctx->rawContext = context; @@ -540,7 +551,7 @@ namespace nfc ctx->discoveryTimeout = std::chrono::system_clock::now() + std::chrono::milliseconds(discoveryTimeout); } - return 0; + return NFC_RESULT_SUCCESS; } sint32 NFCRead(uint32 chan, uint32 discoveryTimeout, NFCUid* uid, NFCUid* uidMask, MPTR callback, void* context) @@ -551,21 +562,19 @@ namespace nfc if (!NFCIsInit(chan)) { - return -0x1E0; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_READ, NFC_RESULT_UNINITIALIZED); } if (NFCGetMode(chan) == NFC_MODE_ACTIVE && NFCSetMode(chan, NFC_MODE_IDLE) < 0) { - return -0x1DC; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_READ, NFC_RESULT_INVALID_MODE); } if (ctx->state != NFC_STATE_IDLE) { - return -0x1DF; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_READ, NFC_RESULT_INVALID_STATE); } - cemuLog_log(LogType::NFC, "starting read"); - ctx->state = NFC_STATE_READ; ctx->readCallback = callback; ctx->readContext = context; @@ -583,7 +592,7 @@ namespace nfc memcpy(&ctx->filter.uid, uid, sizeof(*uid)); memcpy(&ctx->filter.mask, uidMask, sizeof(*uidMask)); - return 0; + return NFC_RESULT_SUCCESS; } sint32 NFCWrite(uint32 chan, uint32 discoveryTimeout, NFCUid* uid, NFCUid* uidMask, uint32 size, void* data, MPTR callback, void* context) @@ -594,17 +603,17 @@ namespace nfc if (!NFCIsInit(chan)) { - return -0x2e0; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, NFC_RESULT_UNINITIALIZED); } if (NFCGetMode(chan) == NFC_MODE_ACTIVE && NFCSetMode(chan, NFC_MODE_IDLE) < 0) { - return -0x2dc; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, NFC_RESULT_INVALID_MODE); } if (ctx->state != NFC_STATE_IDLE) { - return -0x1df; + return NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, NFC_RESULT_INVALID_STATE); } // Create unknown record which contains the rw area @@ -634,7 +643,7 @@ namespace nfc memcpy(&ctx->filter.uid, uid, sizeof(*uid)); memcpy(&ctx->filter.mask, uidMask, sizeof(*uidMask)); - return 0; + return NFC_RESULT_SUCCESS; } void Initialize() @@ -668,14 +677,14 @@ namespace nfc auto nfcData = FileStream::LoadIntoMemory(filePath); if (!nfcData) { - *nfcError = NFC_ERROR_NO_ACCESS; + *nfcError = NFC_TOUCH_TAG_ERROR_NO_ACCESS; return false; } ctx->tag = TagV0::FromBytes(std::as_bytes(std::span(nfcData->data(), nfcData->size()))); if (!ctx->tag) { - *nfcError = NFC_ERROR_INVALID_FILE_FORMAT; + *nfcError = NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT; return false; } @@ -683,7 +692,7 @@ namespace nfc ctx->tagPath = filePath; ctx->touchTime = std::chrono::system_clock::now(); - *nfcError = NFC_ERROR_NONE; + *nfcError = NFC_TOUCH_TAG_ERROR_NONE; return true; } } diff --git a/src/Cafe/OS/libs/nfc/nfc.h b/src/Cafe/OS/libs/nfc/nfc.h index 2ebdd2a4..ea959cd1 100644 --- a/src/Cafe/OS/libs/nfc/nfc.h +++ b/src/Cafe/OS/libs/nfc/nfc.h @@ -1,9 +1,39 @@ #pragma once // CEMU NFC error codes -#define NFC_ERROR_NONE (0) -#define NFC_ERROR_NO_ACCESS (1) -#define NFC_ERROR_INVALID_FILE_FORMAT (2) +#define NFC_TOUCH_TAG_ERROR_NONE (0) +#define NFC_TOUCH_TAG_ERROR_NO_ACCESS (1) +#define NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT (2) + +// NFC result base +#define NFC_RESULT_BASE_INIT (-0x100) +#define NFC_RESULT_BASE_READ (-0x200) +#define NFC_RESULT_BASE_WRITE (-0x300) +#define NFC_RESULT_BASE_FORMAT (-0x400) +#define NFC_RESULT_BASE_SET_READ_ONLY (-0x500) +#define NFC_RESULT_BASE_IS_TAG_PRESENT (-0x600) +#define NFC_RESULT_BASE_ABORT (-0x700) +#define NFC_RESULT_BASE_SHUTDOWN (-0x800) +#define NFC_RESULT_BASE_DETECT (-0x900) +#define NFC_RESULT_BASE_SEND_RAW_DATA (-0xA00) +#define NFC_RESULT_BASE_SET_MODE (-0xB00) +#define NFC_RESULT_BASE_TAG_PARSE (-0xC00) +#define NFC_RESULT_BASE_GET_TAG_INFO (-0x1400) + +// NFC result status +#define NFC_RESULT_NO_TAG (0x01) +#define NFC_RESULT_INVALID_TAG (0x02) +#define NFC_RESULT_UID_MISMATCH (0x0A) +#define NFC_RESULT_UNINITIALIZED (0x20) +#define NFC_RESULT_INVALID_STATE (0x21) +#define NFC_RESULT_INVALID_MODE (0x24) +#define NFC_RESULT_TAG_INFO_TIMEOUT (0x7A) + +// Result macros +#define NFC_RESULT_SUCCESS (0) +#define NFC_RESULT_BASE_MASK (0xFFFFFF00) +#define NFC_RESULT_MASK (0x000000FF) +#define NFC_MAKE_RESULT(base, result) ((base) | (result)) #define NFC_PROTOCOL_T1T 0x1 #define NFC_PROTOCOL_T2T 0x2 diff --git a/src/Cafe/OS/libs/ntag/ntag.cpp b/src/Cafe/OS/libs/ntag/ntag.cpp index 18ed798a..24617791 100644 --- a/src/Cafe/OS/libs/ntag/ntag.cpp +++ b/src/Cafe/OS/libs/ntag/ntag.cpp @@ -26,10 +26,27 @@ namespace ntag MPTR gReadCallbacks[2]; MPTR gWriteCallbacks[2]; - sint32 __NTAGConvertNFCError(sint32 error) + sint32 __NTAGConvertNFCResult(sint32 result) { - // TODO - return error; + if (result == NFC_RESULT_SUCCESS) + { + return NTAG_RESULT_SUCCESS; + } + + switch (result & NFC_RESULT_MASK) + { + case NFC_RESULT_UNINITIALIZED: + return NTAG_RESULT_UNINITIALIZED; + case NFC_RESULT_INVALID_STATE: + return NTAG_RESULT_INVALID_STATE; + case NFC_RESULT_NO_TAG: + return NTAG_RESULT_NO_TAG; + case NFC_RESULT_UID_MISMATCH: + return NTAG_RESULT_UID_MISMATCH; + } + + // TODO convert more errors + return NTAG_RESULT_INVALID; } sint32 NTAGInit(uint32 chan) @@ -40,7 +57,7 @@ namespace ntag sint32 NTAGInitEx(uint32 chan) { sint32 result = nfc::NFCInitEx(chan, 1); - return __NTAGConvertNFCError(result); + return __NTAGConvertNFCResult(result); } sint32 NTAGShutdown(uint32 chan) @@ -58,7 +75,7 @@ namespace ntag gReadCallbacks[chan] = MPTR_NULL; gWriteCallbacks[chan] = MPTR_NULL; - return __NTAGConvertNFCError(result); + return __NTAGConvertNFCResult(result); } bool NTAGIsInit(uint32 chan) @@ -105,7 +122,7 @@ namespace ntag ppcDefineParamS32(error, 1); ppcDefineParamPtr(context, void, 2); - PPCCoreCallback(gAbortCallbacks[chan], chan, __NTAGConvertNFCError(error), context); + PPCCoreCallback(gAbortCallbacks[chan], chan, __NTAGConvertNFCResult(error), context); osLib_returnFromFunction(hCPU, 0); } @@ -118,7 +135,7 @@ namespace ntag gAbortCallbacks[chan] = callback; sint32 result = nfc::NFCAbort(chan, RPLLoader_MakePPCCallable(__NTAGAbortCallback), context); - return __NTAGConvertNFCError(result); + return __NTAGConvertNFCResult(result); } bool __NTAGRawDataToNfcData(iosu::ccr_nfc::CCRNFCCryptData* raw, iosu::ccr_nfc::CCRNFCCryptData* nfc) @@ -370,7 +387,7 @@ namespace ntag readResult->readOnly = readOnly; - error = __NTAGConvertNFCError(error); + error = __NTAGConvertNFCResult(error); if (error == 0) { memset(rwData.GetPointer(), 0, 0x1C8); @@ -430,7 +447,7 @@ namespace ntag } sint32 result = nfc::NFCRead(chan, timeout, &_uid, &_uidMask, RPLLoader_MakePPCCallable(__NTAGReadCallback), context); - return __NTAGConvertNFCError(result); + return __NTAGConvertNFCResult(result); } sint32 __NTAGEncryptData(void* encryptedData, const void* rawData) @@ -512,7 +529,7 @@ namespace ntag ppcDefineParamS32(error, 1); ppcDefineParamPtr(context, void, 2); - PPCCoreCallback(gWriteCallbacks[chan], chan, __NTAGConvertNFCError(error), context); + PPCCoreCallback(gWriteCallbacks[chan], chan, __NTAGConvertNFCResult(error), context); osLib_returnFromFunction(hCPU, 0); } @@ -538,7 +555,7 @@ namespace ntag NTAGAreaHeader roHeader; uint8 writeBuffer[0x1C8]{}; - error = __NTAGConvertNFCError(error); + error = __NTAGConvertNFCResult(error); if (error == 0) { // Copy raw and locked data into a contigous buffer @@ -576,7 +593,7 @@ namespace ntag return; } - error = __NTAGConvertNFCError(error); + error = __NTAGConvertNFCResult(error); } PPCCoreCallback(gWriteCallbacks[chan], chan, error, context); @@ -600,7 +617,7 @@ namespace ntag memcpy(gWriteData[chan].data, rwData, rwSize); sint32 result = nfc::NFCRead(chan, timeout, &gWriteData[chan].uid, &gWriteData[chan].uidMask, RPLLoader_MakePPCCallable(__NTAGReadBeforeWriteCallback), context); - return __NTAGConvertNFCError(result); + return __NTAGConvertNFCResult(result); } sint32 NTAGFormat(uint32 chan, uint32 timeout, nfc::NFCUid* uid, uint32 rwSize, void* rwData, MPTR callback, void* context) @@ -608,7 +625,9 @@ namespace ntag cemu_assert(chan < 2); // TODO - return -1; + cemu_assert_debug(false); + + return NTAG_RESULT_INVALID; } void Initialize() diff --git a/src/Cafe/OS/libs/ntag/ntag.h b/src/Cafe/OS/libs/ntag/ntag.h index 697c065e..68f1801b 100644 --- a/src/Cafe/OS/libs/ntag/ntag.h +++ b/src/Cafe/OS/libs/ntag/ntag.h @@ -1,6 +1,13 @@ #pragma once #include "Cafe/OS/libs/nfc/nfc.h" +#define NTAG_RESULT_SUCCESS (0) +#define NTAG_RESULT_UNINITIALIZED (-0x3E7) +#define NTAG_RESULT_INVALID_STATE (-0x3E6) +#define NTAG_RESULT_NO_TAG (-0x3E5) +#define NTAG_RESULT_INVALID (-0x3E1) +#define NTAG_RESULT_UID_MISMATCH (-0x3DB) + namespace ntag { struct NTAGFormatSettings diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index cb2e988d..33e2cdc1 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -269,10 +269,10 @@ public: } else { - if (nfcError == NFC_ERROR_NO_ACCESS) + if (nfcError == NFC_TOUCH_TAG_ERROR_NO_ACCESS) wxMessageBox(_("Cannot open file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - else if (nfcError == NFC_ERROR_INVALID_FILE_FORMAT) - wxMessageBox(_("Not a valid NFC NTAG215 file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + else if (nfcError == NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT) + wxMessageBox(_("Not a valid NFC file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } } @@ -751,10 +751,10 @@ void MainWindow::OnNFCMenu(wxCommandEvent& event) uint32 nfcError; if (nfc::TouchTagFromFile(_utf8ToPath(wxStrFilePath.utf8_string()), &nfcError) == false) { - if (nfcError == NFC_ERROR_NO_ACCESS) + if (nfcError == NFC_TOUCH_TAG_ERROR_NO_ACCESS) wxMessageBox(_("Cannot open file")); - else if (nfcError == NFC_ERROR_INVALID_FILE_FORMAT) - wxMessageBox(_("Not a valid NFC NTAG215 file")); + else if (nfcError == NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT) + wxMessageBox(_("Not a valid NFC file")); } else { @@ -774,10 +774,10 @@ void MainWindow::OnNFCMenu(wxCommandEvent& event) uint32 nfcError = 0; if (nfc::TouchTagFromFile(_utf8ToPath(path), &nfcError) == false) { - if (nfcError == NFC_ERROR_NO_ACCESS) + if (nfcError == NFC_TOUCH_TAG_ERROR_NO_ACCESS) wxMessageBox(_("Cannot open file")); - else if (nfcError == NFC_ERROR_INVALID_FILE_FORMAT) - wxMessageBox(_("Not a valid NFC NTAG215 file")); + else if (nfcError == NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT) + wxMessageBox(_("Not a valid NFC file")); } else { From eb1983daa6e46dfa09ad76eba86c5b636fe0b826 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Sat, 18 May 2024 17:27:49 +0200 Subject: [PATCH 07/73] nfc: Remove backup path --- src/Cafe/OS/libs/nfc/nfc.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Cafe/OS/libs/nfc/nfc.cpp b/src/Cafe/OS/libs/nfc/nfc.cpp index 818c7339..c6809362 100644 --- a/src/Cafe/OS/libs/nfc/nfc.cpp +++ b/src/Cafe/OS/libs/nfc/nfc.cpp @@ -223,16 +223,8 @@ namespace nfc // Update tag NDEF data ctx->tag->SetNDEFData(ctx->writeMessage.ToBytes()); - // TODO remove this once writing is confirmed working - fs::path newPath = ctx->tagPath; - if (newPath.extension() != ".bak") - { - newPath += ".bak"; - } - cemuLog_log(LogType::NFC, "Saving tag as {}...", newPath.string()); - // open file for writing - FileStream* fs = FileStream::createFile2(newPath); + FileStream* fs = FileStream::openFile2(ctx->tagPath, true); if (!fs) { result = NFC_MAKE_RESULT(NFC_RESULT_BASE_WRITE, 0x22); From a115921b43d39c24fafd387d9c87190168422583 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Sat, 18 May 2024 19:56:56 +0200 Subject: [PATCH 08/73] Fix inconsistency with int types --- src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp | 22 +++++++------- src/Cafe/OS/libs/nfc/TLV.cpp | 12 ++++---- src/Cafe/OS/libs/nfc/TagV0.cpp | 42 +++++++++++++------------- src/Cafe/OS/libs/nfc/TagV0.h | 8 ++--- src/Cafe/OS/libs/nfc/ndef.cpp | 26 ++++++++-------- src/Cafe/OS/libs/nfc/ndef.h | 4 +-- src/Cafe/OS/libs/nfc/nfc.cpp | 4 +-- src/Cafe/OS/libs/nfc/stream.cpp | 12 ++++---- 8 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp b/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp index ff8ba2b1..1ceb16dc 100644 --- a/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp +++ b/src/Cafe/IOSU/ccr_nfc/iosu_ccr_nfc.cpp @@ -71,9 +71,9 @@ namespace iosu return CCR_NFC_ERROR; } - sint32 CCRNFCAESCTRCrypt(const uint8* key, const void* ivNonce, const void* inData, uint32_t inSize, void* outData, uint32_t outSize) + sint32 CCRNFCAESCTRCrypt(const uint8* key, const void* ivNonce, const void* inData, uint32 inSize, void* outData, uint32 outSize) { - uint8_t tmpIv[0x10]; + uint8 tmpIv[0x10]; memcpy(tmpIv, ivNonce, sizeof(tmpIv)); memcpy(outData, inData, inSize); @@ -81,7 +81,7 @@ namespace iosu return 0; } - sint32 __CCRNFCGenerateKey(const uint8* hmacKey, uint32 hmacKeySize, const uint8* name, uint32_t nameSize, const uint8* inData, uint32_t inSize, uint8* outData, uint32_t outSize) + sint32 __CCRNFCGenerateKey(const uint8* hmacKey, uint32 hmacKeySize, const uint8* name, uint32 nameSize, const uint8* inData, uint32 inSize, uint8* outData, uint32 outSize) { if (nameSize != 0xe || outSize != 0x40) { @@ -89,13 +89,13 @@ namespace iosu } // Create a buffer containing 2 counter bytes, the key name, and the key data - uint8_t buffer[0x50]; + uint8 buffer[0x50]; buffer[0] = 0; buffer[1] = 0; memcpy(buffer + 2, name, nameSize); memcpy(buffer + nameSize + 2, inData, inSize); - uint16_t counter = 0; + uint16 counter = 0; while (outSize > 0) { // Set counter bytes and increment counter @@ -118,9 +118,9 @@ namespace iosu sint32 __CCRNFCGenerateInternalKeys(const CCRNFCCryptData* in, const uint8* keyGenSalt) { - uint8_t lockedSecretBuffer[0x40] = { 0 }; - uint8_t unfixedInfosBuffer[0x40] = { 0 }; - uint8_t outBuffer[0x40] = { 0 }; + uint8 lockedSecretBuffer[0x40] = { 0 }; + uint8 unfixedInfosBuffer[0x40] = { 0 }; + uint8 outBuffer[0x40] = { 0 }; // Fill the locked secret buffer memcpy(lockedSecretBuffer, sLockedSecretMagicBytes, sizeof(sLockedSecretMagicBytes)); @@ -193,7 +193,7 @@ namespace iosu sint32 __CCRNFCCryptData(const CCRNFCCryptData* in, CCRNFCCryptData* out, bool decrypt) { // Decrypt key generation salt - uint8_t keyGenSalt[0x20]; + uint8 keyGenSalt[0x20]; sint32 res = CCRNFCAESCTRCrypt(sNfcKey, sNfcKeyIV, in->data + in->keyGenSaltOffset, 0x20, keyGenSalt, sizeof(keyGenSalt)); if (res != 0) { @@ -227,7 +227,7 @@ namespace iosu } // Verify HMACs - uint8_t hmacBuffer[0x20]; + uint8 hmacBuffer[0x20]; uint32 hmacLen = sizeof(hmacBuffer); if (!HMAC(EVP_sha256(), sLockedSecretInternalHmacKey, sizeof(sLockedSecretInternalHmacKey), out->data + in->lockedSecretHmacOffset + 0x20, (in->dataSize - in->lockedSecretHmacOffset) - 0x20, hmacBuffer, &hmacLen)) @@ -258,7 +258,7 @@ namespace iosu } else { - uint8_t hmacBuffer[0x20]; + uint8 hmacBuffer[0x20]; uint32 hmacLen = sizeof(hmacBuffer); if (!HMAC(EVP_sha256(), sLockedSecretInternalHmacKey, sizeof(sLockedSecretInternalHmacKey), out->data + in->lockedSecretHmacOffset + 0x20, (in->dataSize - in->lockedSecretHmacOffset) - 0x20, hmacBuffer, &hmacLen)) diff --git a/src/Cafe/OS/libs/nfc/TLV.cpp b/src/Cafe/OS/libs/nfc/TLV.cpp index 99536428..2650858d 100644 --- a/src/Cafe/OS/libs/nfc/TLV.cpp +++ b/src/Cafe/OS/libs/nfc/TLV.cpp @@ -25,7 +25,7 @@ std::vector TLV::FromBytes(const std::span& data) while (stream.GetRemaining() > 0 && !hasTerminator) { // Read the tag - uint8_t byte; + uint8 byte; stream >> byte; Tag tag = static_cast(byte); @@ -43,7 +43,7 @@ std::vector TLV::FromBytes(const std::span& data) default: { // Read the length - uint16_t length; + uint16 length; stream >> byte; length = byte; @@ -85,7 +85,7 @@ std::vector TLV::ToBytes() const VectorStream stream(bytes, std::endian::big); // Write tag - stream << std::uint8_t(mTag); + stream << uint8(mTag); switch (mTag) { @@ -99,12 +99,12 @@ std::vector TLV::ToBytes() const // Write length (decide if as a 8-bit or 16-bit value) if (mValue.size() >= 0xff) { - stream << std::uint8_t(0xff); - stream << std::uint16_t(mValue.size()); + stream << uint8(0xff); + stream << uint16(mValue.size()); } else { - stream << std::uint8_t(mValue.size()); + stream << uint8(mValue.size()); } // Write value diff --git a/src/Cafe/OS/libs/nfc/TagV0.cpp b/src/Cafe/OS/libs/nfc/TagV0.cpp index 8b5a8143..41b5c7a0 100644 --- a/src/Cafe/OS/libs/nfc/TagV0.cpp +++ b/src/Cafe/OS/libs/nfc/TagV0.cpp @@ -9,17 +9,17 @@ namespace constexpr std::size_t kTagSize = 512u; constexpr std::size_t kMaxBlockCount = kTagSize / sizeof(TagV0::Block); -constexpr std::uint8_t kLockbyteBlock0 = 0xe; -constexpr std::uint8_t kLockbytesStart0 = 0x0; -constexpr std::uint8_t kLockbytesEnd0 = 0x2; -constexpr std::uint8_t kLockbyteBlock1 = 0xf; -constexpr std::uint8_t kLockbytesStart1 = 0x2; -constexpr std::uint8_t kLockbytesEnd1 = 0x8; +constexpr uint8 kLockbyteBlock0 = 0xe; +constexpr uint8 kLockbytesStart0 = 0x0; +constexpr uint8 kLockbytesEnd0 = 0x2; +constexpr uint8 kLockbyteBlock1 = 0xf; +constexpr uint8 kLockbytesStart1 = 0x2; +constexpr uint8 kLockbytesEnd1 = 0x8; -constexpr std::uint8_t kNDEFMagicNumber = 0xe1; +constexpr uint8 kNDEFMagicNumber = 0xe1; // These blocks are not part of the locked area -constexpr bool IsBlockLockedOrReserved(std::uint8_t blockIdx) +constexpr bool IsBlockLockedOrReserved(uint8 blockIdx) { // Block 0 is the UID if (blockIdx == 0x0) @@ -153,7 +153,7 @@ std::vector TagV0::ToBytes() const // The rest will be the data area auto dataIterator = dataArea.begin(); - for (std::uint8_t currentBlock = 0; currentBlock < kMaxBlockCount; currentBlock++) + for (uint8 currentBlock = 0; currentBlock < kMaxBlockCount; currentBlock++) { // All blocks which aren't locked make up the dataArea if (!IsBlockLocked(currentBlock)) @@ -189,15 +189,15 @@ void TagV0::SetNDEFData(const std::span& data) bool TagV0::ParseLockedArea(const std::span& data) { - std::uint8_t currentBlock = 0; + uint8 currentBlock = 0; // Start by parsing the first set of lock bytes - for (std::uint8_t i = kLockbytesStart0; i < kLockbytesEnd0; i++) + for (uint8 i = kLockbytesStart0; i < kLockbytesEnd0; i++) { - std::uint8_t lockByte = std::uint8_t(data[kLockbyteBlock0 * sizeof(Block) + i]); + uint8 lockByte = uint8(data[kLockbyteBlock0 * sizeof(Block) + i]); // Iterate over the individual bits in the lock byte - for (std::uint8_t j = 0; j < 8; j++) + for (uint8 j = 0; j < 8; j++) { // Is block locked? if (lockByte & (1u << j)) @@ -221,11 +221,11 @@ bool TagV0::ParseLockedArea(const std::span& data) } // Parse the second set of lock bytes - for (std::uint8_t i = kLockbytesStart1; i < kLockbytesEnd1; i++) { - std::uint8_t lockByte = std::uint8_t(data[kLockbyteBlock1 * sizeof(Block) + i]); + for (uint8 i = kLockbytesStart1; i < kLockbytesEnd1; i++) { + uint8 lockByte = uint8(data[kLockbyteBlock1 * sizeof(Block) + i]); // Iterate over the individual bits in the lock byte - for (std::uint8_t j = 0; j < 8; j++) + for (uint8 j = 0; j < 8; j++) { // Is block locked? if (lockByte & (1u << j)) @@ -251,14 +251,14 @@ bool TagV0::ParseLockedArea(const std::span& data) return true; } -bool TagV0::IsBlockLocked(std::uint8_t blockIdx) const +bool TagV0::IsBlockLocked(uint8 blockIdx) const { return mLockedBlocks.contains(blockIdx) || IsBlockLockedOrReserved(blockIdx); } bool TagV0::ParseDataArea(const std::span& data, std::vector& dataArea) { - for (std::uint8_t currentBlock = 0; currentBlock < kMaxBlockCount; currentBlock++) + for (uint8 currentBlock = 0; currentBlock < kMaxBlockCount; currentBlock++) { // All blocks which aren't locked make up the dataArea if (!IsBlockLocked(currentBlock)) @@ -274,7 +274,7 @@ bool TagV0::ParseDataArea(const std::span& data, std::vector> 4 != 1) { cemuLog_log(LogType::Force, "Error: CC: Invalid Version Number"); @@ -290,7 +290,7 @@ bool TagV0::ValidateCapabilityContainer() } // Tag memory size - std::uint8_t tms = mCapabilityContainer[2]; + uint8 tms = mCapabilityContainer[2]; if (8u * (tms + 1) < kTagSize) { cemuLog_log(LogType::Force, "Error: CC: Incomplete tag memory size"); diff --git a/src/Cafe/OS/libs/nfc/TagV0.h b/src/Cafe/OS/libs/nfc/TagV0.h index 1d0e88d7..72c321b6 100644 --- a/src/Cafe/OS/libs/nfc/TagV0.h +++ b/src/Cafe/OS/libs/nfc/TagV0.h @@ -26,13 +26,13 @@ public: private: bool ParseLockedArea(const std::span& data); - bool IsBlockLocked(std::uint8_t blockIdx) const; + bool IsBlockLocked(uint8 blockIdx) const; bool ParseDataArea(const std::span& data, std::vector& dataArea); bool ValidateCapabilityContainer(); - std::map mLockedOrReservedBlocks; - std::map mLockedBlocks; - std::array mCapabilityContainer; + std::map mLockedOrReservedBlocks; + std::map mLockedBlocks; + std::array mCapabilityContainer; std::vector mTLVs; std::size_t mNdefTlvIdx; std::vector mLockedArea; diff --git a/src/Cafe/OS/libs/nfc/ndef.cpp b/src/Cafe/OS/libs/nfc/ndef.cpp index 32097cfd..60be5811 100644 --- a/src/Cafe/OS/libs/nfc/ndef.cpp +++ b/src/Cafe/OS/libs/nfc/ndef.cpp @@ -19,20 +19,20 @@ namespace ndef Record rec; // Read record header - uint8_t recHdr; + uint8 recHdr; stream >> recHdr; rec.mFlags = recHdr & ~NDEF_TNF_MASK; rec.mTNF = static_cast(recHdr & NDEF_TNF_MASK); // Type length - uint8_t typeLen; + uint8 typeLen; stream >> typeLen; // Payload length; - uint32_t payloadLen; + uint32 payloadLen; if (recHdr & NDEF_SR) { - uint8_t len; + uint8 len; stream >> len; payloadLen = len; } @@ -48,7 +48,7 @@ namespace ndef } // ID length - uint8_t idLen = 0; + uint8 idLen = 0; if (recHdr & NDEF_IL) { stream >> idLen; @@ -81,35 +81,35 @@ namespace ndef return rec; } - std::vector Record::ToBytes(uint8_t flags) const + std::vector Record::ToBytes(uint8 flags) const { std::vector bytes; VectorStream stream(bytes, std::endian::big); // Combine flags (clear message begin and end flags) - std::uint8_t finalFlags = mFlags & ~(NDEF_MB | NDEF_ME); + uint8 finalFlags = mFlags & ~(NDEF_MB | NDEF_ME); finalFlags |= flags; // Write flags + tnf - stream << std::uint8_t(finalFlags | std::uint8_t(mTNF)); + stream << uint8(finalFlags | uint8(mTNF)); // Type length - stream << std::uint8_t(mType.size()); + stream << uint8(mType.size()); // Payload length if (IsShort()) { - stream << std::uint8_t(mPayload.size()); + stream << uint8(mPayload.size()); } else { - stream << std::uint32_t(mPayload.size()); + stream << uint32(mPayload.size()); } // ID length if (mFlags & NDEF_IL) { - stream << std::uint8_t(mID.size()); + stream << uint8(mID.size()); } // Type @@ -249,7 +249,7 @@ namespace ndef for (std::size_t i = 0; i < mRecords.size(); i++) { - std::uint8_t flags = 0; + uint8 flags = 0; // Add message begin flag to first record if (i == 0) diff --git a/src/Cafe/OS/libs/nfc/ndef.h b/src/Cafe/OS/libs/nfc/ndef.h index b5f38b17..398feb54 100644 --- a/src/Cafe/OS/libs/nfc/ndef.h +++ b/src/Cafe/OS/libs/nfc/ndef.h @@ -39,7 +39,7 @@ namespace ndef virtual ~Record(); static std::optional FromStream(Stream& stream); - std::vector ToBytes(uint8_t flags = 0) const; + std::vector ToBytes(uint8 flags = 0) const; TypeNameFormat GetTNF() const; const std::vector& GetID() const; @@ -55,7 +55,7 @@ namespace ndef bool IsShort() const; private: - uint8_t mFlags; + uint8 mFlags; TypeNameFormat mTNF; std::vector mID; std::vector mType; diff --git a/src/Cafe/OS/libs/nfc/nfc.cpp b/src/Cafe/OS/libs/nfc/nfc.cpp index c6809362..fcb1d8d0 100644 --- a/src/Cafe/OS/libs/nfc/nfc.cpp +++ b/src/Cafe/OS/libs/nfc/nfc.cpp @@ -149,9 +149,9 @@ namespace nfc StackAllocator uid; bool readOnly = false; uint32 dataSize = 0; - StackAllocator data; + StackAllocator data; uint32 lockedDataSize = 0; - StackAllocator lockedData; + StackAllocator lockedData; if (ctx->tag) { diff --git a/src/Cafe/OS/libs/nfc/stream.cpp b/src/Cafe/OS/libs/nfc/stream.cpp index 73c2880f..dd6de7ad 100644 --- a/src/Cafe/OS/libs/nfc/stream.cpp +++ b/src/Cafe/OS/libs/nfc/stream.cpp @@ -28,7 +28,7 @@ std::endian Stream::GetEndianness() const Stream& Stream::operator>>(bool& val) { - std::uint8_t i; + uint8 i; *this >> i; val = !!i; @@ -37,7 +37,7 @@ Stream& Stream::operator>>(bool& val) Stream& Stream::operator>>(float& val) { - std::uint32_t i; + uint32 i; *this >> i; val = std::bit_cast(i); @@ -46,7 +46,7 @@ Stream& Stream::operator>>(float& val) Stream& Stream::operator>>(double& val) { - std::uint64_t i; + uint64 i; *this >> i; val = std::bit_cast(i); @@ -55,7 +55,7 @@ Stream& Stream::operator>>(double& val) Stream& Stream::operator<<(bool val) { - std::uint8_t i = val; + uint8 i = val; *this >> i; return *this; @@ -63,7 +63,7 @@ Stream& Stream::operator<<(bool val) Stream& Stream::operator<<(float val) { - std::uint32_t i = std::bit_cast(val); + uint32 i = std::bit_cast(val); *this >> i; return *this; @@ -71,7 +71,7 @@ Stream& Stream::operator<<(float val) Stream& Stream::operator<<(double val) { - std::uint64_t i = std::bit_cast(val); + uint64 i = std::bit_cast(val); *this >> i; return *this; From 964d2acb44c64015637d1a8713cc2e96bf53bb48 Mon Sep 17 00:00:00 2001 From: GaryOderNichts <12049776+GaryOderNichts@users.noreply.github.com> Date: Sat, 18 May 2024 20:47:09 +0200 Subject: [PATCH 09/73] Filestream_unix: Include cstdarg --- src/Common/unix/FileStream_unix.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Common/unix/FileStream_unix.cpp b/src/Common/unix/FileStream_unix.cpp index 2dba17b7..4bc9b526 100644 --- a/src/Common/unix/FileStream_unix.cpp +++ b/src/Common/unix/FileStream_unix.cpp @@ -1,4 +1,5 @@ #include "Common/unix/FileStream_unix.h" +#include fs::path findPathCI(const fs::path& path) { From c913a59c7a7140eb7b148611362eee519217dc27 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Wed, 22 May 2024 04:11:02 +0200 Subject: [PATCH 10/73] TitleList: Add homebrew title type (#1203) --- src/Cafe/TitleList/TitleId.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Cafe/TitleList/TitleId.h b/src/Cafe/TitleList/TitleId.h index b7f63b13..073472d9 100644 --- a/src/Cafe/TitleList/TitleId.h +++ b/src/Cafe/TitleList/TitleId.h @@ -13,6 +13,7 @@ public: /* 00 */ BASE_TITLE = 0x00, // eShop and disc titles /* 02 */ BASE_TITLE_DEMO = 0x02, /* 0E */ BASE_TITLE_UPDATE = 0x0E, // update for BASE_TITLE (and maybe BASE_TITLE_DEMO?) + /* 0F */ HOMEBREW = 0x0F, /* 0C */ AOC = 0x0C, // DLC /* 10 */ SYSTEM_TITLE = 0x10, // eShop etc /* 1B */ SYSTEM_DATA = 0x1B, @@ -43,6 +44,8 @@ public: return TITLE_TYPE::BASE_TITLE_DEMO; case 0x0E: return TITLE_TYPE::BASE_TITLE_UPDATE; + case 0x0F: + return TITLE_TYPE::HOMEBREW; case 0x0C: return TITLE_TYPE::AOC; case 0x10: From 523a1652df4e8e7a18466e1d2668573dc06909af Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Wed, 22 May 2024 04:23:33 +0200 Subject: [PATCH 11/73] OpenGL: Restore ProgramBinary cache for GL shaders (#1209) --- src/Cafe/HW/Latte/Renderer/OpenGL/RendererShaderGL.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Cafe/HW/Latte/Renderer/OpenGL/RendererShaderGL.cpp b/src/Cafe/HW/Latte/Renderer/OpenGL/RendererShaderGL.cpp index 3d46f206..cae53140 100644 --- a/src/Cafe/HW/Latte/Renderer/OpenGL/RendererShaderGL.cpp +++ b/src/Cafe/HW/Latte/Renderer/OpenGL/RendererShaderGL.cpp @@ -23,16 +23,13 @@ bool RendererShaderGL::loadBinary() cemu_assert_debug(m_baseHash != 0); uint64 h1, h2; GenerateShaderPrecompiledCacheFilename(m_type, m_baseHash, m_auxHash, h1, h2); - sint32 fileSize = 0; std::vector cacheFileData; if (!s_programBinaryCache->GetFile({h1, h2 }, cacheFileData)) return false; - if (fileSize < sizeof(uint32)) - { + if (cacheFileData.size() <= sizeof(uint32)) return false; - } - uint32 shaderBinFormat = *(uint32*)(cacheFileData.data() + 0); + uint32 shaderBinFormat = *(uint32*)(cacheFileData.data()); m_program = glCreateProgram(); glProgramBinary(m_program, shaderBinFormat, cacheFileData.data()+4, cacheFileData.size()-4); From b048a1fd9effb59a212c8e1c8ad5069a953f3f3b Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 22 May 2024 05:08:03 +0200 Subject: [PATCH 12/73] Use CURLOPT_USERAGENT instead of manually setting User-Agent --- src/Cemu/napi/napi_helper.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Cemu/napi/napi_helper.cpp b/src/Cemu/napi/napi_helper.cpp index e498d07f..164de7e5 100644 --- a/src/Cemu/napi/napi_helper.cpp +++ b/src/Cemu/napi/napi_helper.cpp @@ -388,9 +388,8 @@ bool CurlSOAPHelper::submitRequest() headers = curl_slist_append(headers, "Accept-Charset: UTF-8"); headers = curl_slist_append(headers, fmt::format("SOAPAction: urn:{}.wsapi.broadon.com/{}", m_serviceType, m_requestMethod).c_str()); headers = curl_slist_append(headers, "Accept: */*"); - headers = curl_slist_append(headers, "User-Agent: EVL NUP 040800 Sep 18 2012 20:20:02"); - curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(m_curl, CURLOPT_USERAGENT, "EVL NUP 040800 Sep 18 2012 20:20:02"); // send request auto res = curl_easy_perform(m_curl); From 917ea2ef234f583032dab84a6f0f371709823ce1 Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Thu, 23 May 2024 17:48:04 +0000 Subject: [PATCH 13/73] Update translation files --- bin/resources/de/cemu.mo | Bin 65048 -> 65571 bytes bin/resources/es/cemu.mo | Bin 62413 -> 65733 bytes bin/resources/fr/cemu.mo | Bin 63716 -> 72178 bytes bin/resources/hu/cemu.mo | Bin 62167 -> 71267 bytes bin/resources/ko/cemu.mo | Bin 69116 -> 69784 bytes bin/resources/ru/cemu.mo | Bin 46500 -> 89284 bytes bin/resources/zh/cemu.mo | Bin 56868 -> 58070 bytes 7 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/de/cemu.mo b/bin/resources/de/cemu.mo index 918fcd3546c99e9c385fe726bbd91c9e95b19270..8dc4e8cc9ec032d5dcd38af5359b3246398a7e0a 100644 GIT binary patch delta 15405 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 delta 14957 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 T2MmbJT>bS>-^?0UevSS=9Aq~l diff --git a/bin/resources/es/cemu.mo b/bin/resources/es/cemu.mo index 4f78fdcd0d63921934cbce6338ed108d179fbc24..856049de037de70de90a2868e6da991fe25d1235 100644 GIT binary patch delta 20815 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 diff --git a/bin/resources/fr/cemu.mo b/bin/resources/fr/cemu.mo index d31685fca1c4072f58d2fe0e0b467e562a64a205..8328991f01eb915b5ea722452ccefde985d6416c 100644 GIT binary patch delta 25093 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
    )%-I&qz1^!$qKa`*}1O5{fgdrTv(h-<5T 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 Cn8+#s delta 17309 zcma*u2Y405zyI+)X^;YhgpyDX9i#{-y+h~_ngpb$oIoHsfs=$La41Sql(Jw{Kt#HN zg+mvlDhPt8SV2G$L}@CbsEGId$qxU^z0d#N=kD{HyyiPQyF2rp*$u+;`!x{q{V&1( zb1@-HEsm2xmQ@btm$IxQ!IpKmj#@1%uBBxa!FY_q8W@R>VmP)nb~8R^9ER#Q3d>?H z7Q=;D6kovz%ko=qniIQG4>*P)_yxw`SuBhg70StT(Vt7CPH#-3OLhZ%FQ zD)C&@gEwOs?!gG2Zylmih=!A>8GVj=&;`_kel_tO)P)hPoq-j{SmLV4OsuA;fp$P$ z*8_FGz9t@mdQJ)|Go!IE&$q@?QEL3C2QNT%T!os^MpR~Yqh@>rHIr{p54eVU;P0q` z+%*m}tI=K=HNY;Y>-wNy4;oBGYn*C2jKetM$tJc@16Ym)141p)Zq)VrP!l+U zO8pncYp4gh+B%tvK+U`YYH1&8Oa3+DRy1e;T~H}aLfv>YD&;=ZjV7T+J`?r8`B)m4 zBm36cit2Y9HIUP&2mXj^zm2-zJrjqv^E)Xk+RnLAJn8`rP&00UN_7|1g#%Cz8irb` zJgkUwP%~O@+IOP{@F6Ob-=g~cg_>}g_Ra*W`Kf5_>SB3(1l6Gr>cK-$85x60z54gT zg~(fKeTT}#Wz==QqcZ2};9M7u6^P5Du5XFWu_sPN|6D5ZRKgOR2f9&9@h}EsD-*ZF z$B28OX1WITpdB~|_n~gwxuY|nnW%n?kuh0okuKI1BnejCP7H?UTiH~Us!do44`3)> zL@mXS7=pJ@H@t&dn#j&hO5?B*aZ^-3FY3A+RO+XqCiFZO!ZoP9u&F@Se;XC0_+8^C zs0%KmHqURU2U=a66c<6ghHvl)Qy9>IyVeQ^(%+kD|N9kc1LA!CXU2>OvHb2G$wYltY)|i`{OMP$FAMU z|BF=mQ7MeqjlZKZ@-J#BVtY8Li#Jw6wbwE}jQYkmGUxlC?)NxqbEaSv`mhlCQTLzI zgY|DiWtll~1|x_snhrONK|P%%DTVo*uZs0?57NcDj;xZ^oM~&#XQGy187ebx;6r!_ z_23)GN74%FMgDtJY1hk1>0;D-{T6DZ2T=n$hFXfxup*vCH{L}(s602>jPmobQ5pEV)!Z6z>BB{NAhtf zgQZcsy&>u?>4n;?9*n_Rru}*3{(fr{75-=KOcQ>o~|OHi9yF_wOuHwF{&QJjbkdA_xoN)x<*x^dhfXN?n# z-BD}Y*Te&`B=Im*CUQ{^oQ=AEH)=^PqbBkzYJ#DIolKO$DB_yv*KTY?r7SkXD%c-O z;Uwf6Z7oEN`~)_~KTtDmIK&y~P-7ZuZ;U~u{0Y-O5A|tZXyPr{f_T>u@~_=|+nl(I z8hP+gXU4^_3~>q68b6GhQ3upgB%v~sY~pkbCmv_wCr~q;je2`*Ov7a+wuX^^?efCI zoIfPSqcYGMt6>UifOD}rEh+BF zk8o}fkGgSnjKjvL8}-5<9Dti~5Nbe$lby9MhGmH>p)%15mBIF?fhA&59E=*sXjFzK zqMqlUNkyq#jM_{qP$_>CHKSvw&2tWQ;Vsky{=wN8o?=-Oa3Sh-EzFO8n2GINmbD)> zksy!ry@){FuOWu&{qIagspyFsKoTkgqc9X_pk_P=)$cjf0P;~cUXAMiI%7g7CRNA=%|VYmx5 z!2PHJeuz4M8kOmxL=7~-#Dh>vk&;3Fm8xvh@C1et&&5i(08ioDs2R>2?X2+}EKIxz z`4(9(nfQ0q0RP287(2%4R~B{Osu+&7F%%n(A^#Pqw4gy>yg{f7$DuBmgt}llY6eeZ zd3*(x>UUB7&Y-?%-=St!A=9xo>V8e}5p0VIn2Q~7m)~^w2bJ>BEa!%?=q4_M5!eDX z<4)#$FB2!B9xxo0p^>P8Wum^6^Dq*Zqn2<37QvmU4EW!tq8WUMy6{shjXz@^7WO)O zU>fR%Hfn9xVG%rzmGLXo1MZ?S8I{eiZmfdZw0*D&j=_SZ#oBuR-=?CD-=KaAMvm2c zNqVpyjz=xgPSn7@z=!ZA>O)m#obzF6gc`_D)BvZOcr7Y(N3j@wjJn^q7_Rr<@;N6W zusC5ERLbjM6gEN4Gy$~-`l4ny3X7s2qwzV^bt|zEzJ^-s3s@7YsI{Dj8tEd`w|$j4zt^-MHl8-^-=o&}Dr!J~pzafv>kPaoY9i$@7Hgr-w?e;C z)6p~}8k10&7>2rVEb79ksPDrf)PR?u-i~D^UW;1eO{h$qLS^m@YR!Mdbr{LU(q7w= zNB-SZKBA$bi%o~kh@<(9(h|F2Uz~$({Mz_8s$aQ@+!T{A3ENI``sHI!;+YH7cD^nZG2)APhPJYH&b3c|Ko{OQl0^8$i)POG=?_wBn%zS5H@u>Th zN53wtY#M5!ZdljE?TwvLDeHj|_!#QC;iylr2bGcOs1Mgh)ROK(ZRVq>{-2{Bd=?8n z!Sl&~9U81>oiA8jREK7$2ew8sXLUl&pet$<^~RzuKDDR^zVV#%z<03-@d?zDoWV%E zX1rqzS>XIKi(WwfwRx)0pbOigHd`WUrkSWzPC%{gv#0^C#Im>s_4@5c_4@!d@bjqq z{($u{WT7*lMyO5M36<%gekw|x4~yY4)D2$8q8LC8b^};=Q|+-^;-j})ThCR8sTcx12&m>JL-e754Dz`Vhucx>Q~5i&KF0e zyc%kW+M_br8Dp>)R>Kt3(#^%fdjHo@sY1hMR0cjq?as3%{u{McVN0AfEQ{(_2Q{E3 zsLj~{OJFzD8V^T3a6D>BmZK)P9d-YGSXA%-IVzgTb<~ajFmcdQCyqi5ur#V)3oMBp zQA;uyHPak);}fV4({j|?wGB1n?@$A~huRw@`8xS)QF(-l9xxO&fElO<%}34bdDMtk zpw@a1YEOKCrSL0^#-DLKy7HatCSnOo@0cM}ac5~Q^&WuZdTn+l9RFZsJCd}OXOdl*l%f& z6_+`GA2<|s!5gTKzo0f<>~d!}w?Jj69cr_6M{Tm+sMMxnOPql{@LdeX$Q90>DTd{U z%lfG(bxlz>>}opnL(OltTvY!x#%;#;QJe7#)QvBqQf$5K+&3CEfHGJWE1(AK z??6Q}OG2&vG}NA$hYw=_-FOkzFJz^2z78tINmvnwVFjFqx^4wF!xN|n7J7wWTv!K3 z;(DB^_rKUGb}|i%F%Q2)%_Q+v)(r=tHeuvyXS2ni9#jiMu?uP-iKq-FnRo>1x>2YB zWTP^=1oaxO!Z^MEubC5vP#HLd+O=me8GlCIxbGV0K|@gI(@?vA66(5zcm|hY1Dvpy z!Q)08j3Misy)go{NvB~+o^Rz-(FL2a44%jC_!oMx(|YH>kT#(%{2D{>7P|2chGF~$ z=kNdASeLj5s{dsC2yN8fN!{rDWmGQu%hRx#ie_{igYg2!;`dk>@1SPrdd+!I6vh+B z8tbB#tTTpVKYSF2qLyf}IsXFc^<87)z-#1R@BKa+I^$>90?WVd?1jfMoA@KtW@@v^ znZYE~FO#RyjW3`cxC@oR8`uiNH#_}1V^!jTSQDpWWn8zJ{MV-P0S&tFHde>zHyoRw zzG%Zx=Rd$7t;^Dd~AC!&^OIPyPhB0ol9E&tojmv1o^ zqG1DSq;H|#`_E9j{TFm&^cJVR0cr_)qSk&S7C|pI!711tw_+pw!^Cy@93~KtHTt(v zQOCcqCAQkiuS%SZ-SHSchNZR@`~#RZ2DK-)q8_{#HSoix{X#EGn_@u=wH;#!*)40=!@Fb15qPSLESJLYv2UbTeK2& zpS>7?7f=(piqUuzi=nmKiKDUL_kSfSx?z2sg-uZp+=Hd?ebfxU!A4l(9cQV!p_XC} z*29HZ6!)PXbP_e-bEtt`MGdUz9%lgY=-0>|qB0y?;4NH+>bPL9)A1$L4PVDf_zr5o z-(V14!lQTvHK4cObyB<=wPc@Q2nO$Sz7vH}OHg7T>mNs@4h=Eb5erg{dSD8cL?0>> zb5VO^CF;7vsF{6(y3X3~WFP`p5XR%PxC5tS#{>L}I=+viT>PQupx>Fv{6o&)&n>}P zbU2FIJinr5bO$wa*J0;RN>xyMr86oMi6&0MGQ>ko`$TjTPe)~Rm1*B(-0G)NjT3uN zGyD#9gFmq^mUz#}#7NXqO~6o`i`o;t^>L^Hm&f8*6*X{w zV=5t3TB9!LfJ$j1j>mBrj8{;b>l*69zfF7a5oh3qupI3b(2XsyA`ZbYoPu?57AD|Z z$m{O6Zd1_*BbXheR6lHNirQT5P&Y`xQ0$J{8+}lFAQ|=G$*85vN6qj6DkGno^EXi$ zi~7KsU}@!3tboBEIyb6}g^256I5tM5x-Dv8 zBaDm9`NPNcQFE!PC6M#Mm^An4RMNz-$c!L z7iwS!u_zuxE!kJ7=bXp7cooZI{72+p?{|}roEh{)jW7$9qNh*;n2%cPeAHX=7VGljh6Z>S>tVf59Eai~#PhK) z?#FHz`>FF=aTw}>0jz=-uq}q1c0N=cu_p0o)XZ(v01lY=s-KEd8vmKIIjW#8Xoj(v zg4&$pQER*qqwy^)fd`DAoAXz(I_;64JA0-fDr0@HIF7>RI2n7Oe;bvSRD!;6K0IxV zgHTJ5XPkms<5{TJZ9eKdu@SY#A7TuCf~D{hs{dWn9{QzoJ`S}Q5l+Q;yI|zvk*0-H&CDE{iyH7cc|+xq4v&o)aJW`8gR&0P6lffsP(6! zP1F^)Vm4~6s($T!$@*fhi~qbvrS8fZXYH$;b=G3+qIco(&#?XNgr*zQ+Ye|}x+CgM!2i63Jh z{0nzsj~^K%hW+F$(O!%qK8L08D#l~*Rp%`zkB<_!M4cap+C%fMvi_REdK!GV3#($C zYt9V&7;|tE?MqQND0bbsaSc@ePN>X`LEUdAhT#&dfy=Nb9zwk(#cwzht?H+u3lp#r z4#q;b2z7%OQJHzgxCWKd4Y(V(U`fon>11LSDr0M~4xT`r|H~Ntvva@dxSDqVLsSk2 z@yh`dXs~ZNYyTw{BQExvvj-|-H{xzs7nfppJcK00Dt6oXEqE7|0oU(N24hi6+z3Zu zbJP;AMmDM6Iz&Y?{Q))88^&LazoTyO7slbgsQz((IGL!3g^6pS2HX&JzCBjPuBgqJ ziP5+oi{eg<;rZ4PD!SlnjKFWPI$pz;7<!eG=$OcF4n`X z6h89>AE9~Fuhaey@nCF&dY2baf;jgOZEv9tUT-Uzc(#c@rTzs)zdsI9x>Iyi<$PPp zx0-)D%CiJse3K5Nskg>;#OtW*mq;th%fvcfp}bFg|45-v^aHWl`Hoppv_FPh)xg0U zZZ)LeV%l3#R#NUdmcZC3B;()Mrwkh}BKoZ@7~3J!QD=$g!FF4a!~W8!1uL zA3PcojD4_y`U>u;W4qS>Yc445#MWF+%%Loy?LBj&&Yb(poR2VVpAmm(ZlY87XrDoO z`hm7t#E+Z)>QjLdpy(r1+q8Fbk^f$5q~RiV#%C#2sE?u8)b%-i@aWD%rqbTrG)|`; zLR($)@F$3MBvEcs5{O4|t_k%Gl$z9E#X;()^lP+!@zroT;njx&F1=J*mT z(_tcIiW&X=b25~&fOD73O@72QQ`h+n%0dcX?t)`3Keuy!mWd1F08@7$|08+OC88lX z-<(Y3qTXfzwK0siF}{MkDLU#=>Qcs29;59`%H#AIgLOw+lhZQZIx;B(WfNwP|7>hYfyC5#|X}k zqrR1T3+nA%1<7Fd6X^edq@yiG)h7IS17D@{S-gKl5{DBs=X?k1Z>uxMY)UQa4Jo&1 z*YPN&8gWk(uO$vO^|G`V*Yo{Uj?vf@tKw10OVnH98r_AX5oHtcb6A2>n_>~i(eHgq zKH)&(yOa&Y_m2;WYf*HRq68Pz_>|KpTXk)QM+lCYi{7SlFr_~2g-lx^;xm*Pv^_-8 z@eJi3+H{nm^eL$EnsH4RbKNvNKzo9T)#YzW5fgvI^?H86QIudQwJGMNnWj@Z@wb%T zroFFe|Co3x{bPt9rZggMMSPSJP0>-6QjAiE^S3!am*O(l>pX|w+C^i4vY4n3mZIcR ze}j&zDA%aJOxv5(=TmkOKX^W}I=hEdWvmqdB+2qkVnzcoS3{|j@* zL&J*{9p`Zno%&J_q4=oRzz&rA$0XXa=oduUNd2%mae#U<Uw! zsc)t zl(&eV#LM^tSA|p0F;^eQZnW<)@n+29ypDy&bJRbeUXF5*z5|JeVol0w>ZkDL{qG~O zRhEV~D7&~|ALTh+O#I-HOt6jiwzRjTzJfyf6zApj}v<+>xd^% zn$h;1{(N-*n8C>l1lg!#6Rx6TzKPZLJLO?jI4U|=qv?}t`aFcOlz4M)r?DM<`kDF~ z>WAp_H%98?tm8DnSCmxhU1^(u&k)}~PMOLFw7tOjmXzlyt4#Z*`yJ7yUB@GocPZ5= zhfP}x>L;kbVq$+YD(kuF)11(;?SY3z5qG6rp=_bO2zICF_?$QfGq`@LxtZGEqrMWC zsV|}E$T2ri-OIT+e37z% z`Z&%t!UvD(1UqPZjdIRh`xb4DDfcMr^~Zd-xu_Zq-%#okr%`%P*YOA!PBRz(WmNkS z{owe;!Fmn*aov7gZrUc>L)+hr@p&@58M*FG8JV8C?NU=cIXU*~4)X%765ey!ah-Yx zCwA&>FYRR8tvgSR8I?W3o$gD{^0>$PveUW3XYcIXCs4LaRB(8PcWhp+yIt?Dfy&*R z1-sn#ki%VrJ%KlevSMNaWto+czBS$i7 zhG3ueJ!aR*NsRYoktTPF&yzf6Y<7k>*Xo&(;r6l?_PaT2;?YWETArL-ccLd_7E$t^QtWxq3X zpxtEF`as*Kst4J3=bR4QnHz8gu0Gu@IMDyOmx7}*axE0E%Gr)TGR)2!^=vGr0iyzYz~cWSoJmp3-olcudO ziY<`g9iN<;k><|HnC!9Nd%0ux|IhU~9uGS<>;H5QD@WTrC)MZic?ef&{K) zH5`{$3N9bQTMmkXc7 zXJ=XM^4w!H^K!J>I!A2^{~mc90!)JHuXb`%Iw8o#jFHg1=7%7Tql$6!_!b<=`EW!LFx6 zZ10BBJC+u5k;}5DB6mC+?y4N*?#~wDZPe;ycvG289x>mAoIKxn4?+GLk*?iAZg2kR zC|5a`d&it8*9g~+@g0dm`Ul_H|K&0N jz1#ozDD$20SpWGh)?kzSlD$@TUWQeWs{B6ju1Ws^Ce{8H diff --git a/bin/resources/hu/cemu.mo b/bin/resources/hu/cemu.mo index d7f761fe66fa9a72d7e0ec3fed2c6a21687de3da..dd3d2fbc74157e457fac7fefe3356bdbfbbe9e11 100644 GIT binary patch delta 26825 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 delta 18303 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 delta 16937 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 diff --git a/bin/resources/ru/cemu.mo b/bin/resources/ru/cemu.mo index f89098fdfc7ffa83f01393cda3ecb5ffe9f4ea3f..619ba6783440be10ac2b2c0c77cda8523926e8da 100644 GIT binary patch literal 89284 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 N&hx9^FD!o_{Sz8)Lk0i< literal 46500 zcmc(o37lM2mH!`#g3Tg`EQ06@NJt>vSy+UygpiF$5<)r&C?Hh2tCNE6s-~7KAesVm4=e}33syiV%|M~nYy#Bp+ z-+lL8DtBR((f5k+qVtNTY${uNR5MP2Z@Y-AL@8q9&O1($$SiI#(H z;Emv`z;)oh;631;;D^B1fgc554Q>PX20slR2!07vd*252{I9@+!QX+e2lqKPirxao zp!%Bzz8ahls^5!1{)>9}qvvk}_1ybGwf_-N^`8et$BW=W;6H+T{->bm`44a}@Gqd+ zdDW;WdJVV_sOy74-9G}{2Rs@)92^Vo4ZaQ3I66V~+XbqA4HQ4F0Y(3NLACn>5eUJmNHt3l1%Dp1ee0E(Y$13nVUp9RIo7eTfAHBkKh9;k8r4%`onj`Mo^ zgKF<6@HOD^pvHF+cqljxJOsQ1RJ|&wey;&Fj&gbPOo^PX)#A3qi?40TiE>gL>`;a6b56P|tlER6oB3Zv_7Y zYTT>a;Ssn76uop{_TKDaM<0VsOA!IQx2K(+TAsCHfq_!6l8z8df+py>KD zxDVJi*4sM-6n~EfHI5EYbWZ|B#}rWfUI@x`To$gc0X6;`!HM9b;A!A@!MB1(j&u4K zfa3Q$Q1kj>@Br}Rp?n8;HP>GSYvA1R@D%(EsD7gf=oZihYJ87?8rKt`#{B{){(K%B z3I2U3|4&f-`mb=k&j~)xgTU8P{w7fEbb#W=+2Gya91v2XAAz&LLr;vN8DKXkdHEQ4 zCir*YTyRe&L2|MHJQ}QklE=G2*^?JQ(fLhK#XepOZk%(>p

    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 68/73] 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 69/73] 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 70/73] 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 71/73] 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 72/73] 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 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 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 73/73] 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;

    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 d=1Gd$*wcs diff --git a/bin/resources/zh/cemu.mo b/bin/resources/zh/cemu.mo index f9825b24febbfff53509fcd05588d31d42fd36e4..69e06e572cbc240cd3fa396bb0fb323e9dfb62cd 100644 GIT binary patch delta 18614 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 delta 17359 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 From 149fe10a4e0006963956d5dc9d103dc731c1cb6a Mon Sep 17 00:00:00 2001 From: qurious-pixel <62252937+qurious-pixel@users.noreply.github.com> Date: Fri, 24 May 2024 16:48:17 -0700 Subject: [PATCH 14/73] CI+MacOS: Use libusb dylib from vcpkg (#1219) --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1b78b1fb..d5843c37 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,7 +100,7 @@ if (MACOS_BUNDLE) 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 "${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 /usr/local/opt/libusb/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 /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}") endif() set_target_properties(CemuBin PROPERTIES From aadd2f4a1af788d208a38a2d23161a4de517083a Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Sat, 25 May 2024 01:48:53 +0200 Subject: [PATCH 15/73] Input: Assign profile name correctly on save (#1217) --- src/input/InputManager.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/input/InputManager.cpp b/src/input/InputManager.cpp index d928e46c..64b238fc 100644 --- a/src/input/InputManager.cpp +++ b/src/input/InputManager.cpp @@ -472,15 +472,12 @@ bool InputManager::save(size_t player_index, std::string_view filename) emulated_controller->type_string() }.c_str()); + if(!is_default_file) + emulated_controller->m_profile_name = std::string{filename}; + if (emulated_controller->has_profile_name()) emulated_controller_node.append_child("profile").append_child(pugi::node_pcdata).set_value( emulated_controller->get_profile_name().c_str()); - else if (!is_default_file) - { - emulated_controller->m_profile_name = std::string{filename}; - emulated_controller_node.append_child("profile").append_child(pugi::node_pcdata).set_value( - emulated_controller->get_profile_name().c_str()); - } // custom settings emulated_controller->save(emulated_controller_node); From 1ee9d5c78c1c5216c92764977363fad38b0d4f0b Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Mon, 27 May 2024 01:24:24 +0200 Subject: [PATCH 16/73] coreinit: Tweak JD2019 workaround to avoid XCX softlock --- src/Cafe/OS/libs/coreinit/coreinit_Synchronization.cpp | 2 +- src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp | 8 ++++---- src/Cafe/OS/libs/coreinit/coreinit_Thread.h | 6 +++--- src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp | 9 +++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Synchronization.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Synchronization.cpp index 9e5de19e..c144c384 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Synchronization.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Synchronization.cpp @@ -310,7 +310,7 @@ namespace coreinit currentThread->mutexQueue.removeMutex(mutex); mutex->owner = nullptr; if (!mutex->threadQueue.isEmpty()) - mutex->threadQueue.wakeupSingleThreadWaitQueue(true); + mutex->threadQueue.wakeupSingleThreadWaitQueue(true, true); } // currentThread->cancelState = currentThread->cancelState & ~0x10000; } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp index fbf498db..b53d04ed 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp @@ -758,14 +758,14 @@ namespace coreinit } // returns true if thread runs on same core and has higher priority - bool __OSCoreShouldSwitchToThread(OSThread_t* currentThread, OSThread_t* newThread) + bool __OSCoreShouldSwitchToThread(OSThread_t* currentThread, OSThread_t* newThread, bool sharedPriorityAndAffinityWorkaround) { uint32 coreIndex = OSGetCoreId(); if (!newThread->context.hasCoreAffinitySet(coreIndex)) return false; // special case: if current and new thread are running only on the same core then reschedule even if priority is equal // this resolves a deadlock in Just Dance 2019 where one thread would always reacquire the same mutex within it's timeslice, blocking another thread on the same core from acquiring it - if ((1<context.affinity && currentThread->context.affinity == newThread->context.affinity && currentThread->effectivePriority == newThread->effectivePriority) + if (sharedPriorityAndAffinityWorkaround && (1<context.affinity && currentThread->context.affinity == newThread->context.affinity && currentThread->effectivePriority == newThread->effectivePriority) return true; // otherwise reschedule if new thread has higher priority return newThread->effectivePriority < currentThread->effectivePriority; @@ -791,7 +791,7 @@ namespace coreinit // todo - only set this once? thread->wakeUpTime = PPCInterpreter_getMainCoreCycleCounter(); // reschedule if thread has higher priority - if (PPCInterpreter_getCurrentInstance() && __OSCoreShouldSwitchToThread(coreinit::OSGetCurrentThread(), thread)) + if (PPCInterpreter_getCurrentInstance() && __OSCoreShouldSwitchToThread(coreinit::OSGetCurrentThread(), thread, false)) PPCCore_switchToSchedulerWithLock(); } return previousSuspendCount; @@ -948,7 +948,7 @@ namespace coreinit OSThread_t* currentThread = OSGetCurrentThread(); if (currentThread && currentThread != thread) { - if (__OSCoreShouldSwitchToThread(currentThread, thread)) + if (__OSCoreShouldSwitchToThread(currentThread, thread, false)) PPCCore_switchToSchedulerWithLock(); } __OSUnlockScheduler(); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h index fdbcfea7..8b144bd3 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h @@ -126,8 +126,8 @@ namespace coreinit // counterparts for queueAndWait void cancelWait(OSThread_t* thread); - void wakeupEntireWaitQueue(bool reschedule); - void wakeupSingleThreadWaitQueue(bool reschedule); + void wakeupEntireWaitQueue(bool reschedule, bool sharedPriorityAndAffinityWorkaround = false); + void wakeupSingleThreadWaitQueue(bool reschedule, bool sharedPriorityAndAffinityWorkaround = false); private: OSThread_t* takeFirstFromQueue(size_t linkOffset) @@ -611,7 +611,7 @@ namespace coreinit // internal void __OSAddReadyThreadToRunQueue(OSThread_t* thread); - bool __OSCoreShouldSwitchToThread(OSThread_t* currentThread, OSThread_t* newThread); + bool __OSCoreShouldSwitchToThread(OSThread_t* currentThread, OSThread_t* newThread, bool sharedPriorityAndAffinityWorkaround); void __OSQueueThreadDeallocation(OSThread_t* thread); bool __OSIsThreadActive(OSThread_t* thread); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp b/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp index 68cb22b3..b33aa888 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp @@ -128,7 +128,8 @@ namespace coreinit // counterpart for queueAndWait // if reschedule is true then scheduler will switch to woken up thread (if it is runnable on the same core) - void OSThreadQueueInternal::wakeupEntireWaitQueue(bool reschedule) + // sharedPriorityAndAffinityWorkaround is currently a hack/placeholder for some special cases. A proper fix likely involves handling all the nuances of thread effective priority + void OSThreadQueueInternal::wakeupEntireWaitQueue(bool reschedule, bool sharedPriorityAndAffinityWorkaround) { cemu_assert_debug(__OSHasSchedulerLock()); bool shouldReschedule = false; @@ -139,7 +140,7 @@ namespace coreinit thread->state = OSThread_t::THREAD_STATE::STATE_READY; thread->currentWaitQueue = nullptr; coreinit::__OSAddReadyThreadToRunQueue(thread); - if (reschedule && thread->suspendCounter == 0 && PPCInterpreter_getCurrentInstance() && __OSCoreShouldSwitchToThread(coreinit::OSGetCurrentThread(), thread)) + if (reschedule && thread->suspendCounter == 0 && PPCInterpreter_getCurrentInstance() && __OSCoreShouldSwitchToThread(coreinit::OSGetCurrentThread(), thread, sharedPriorityAndAffinityWorkaround)) shouldReschedule = true; } if (shouldReschedule) @@ -148,7 +149,7 @@ namespace coreinit // counterpart for queueAndWait // if reschedule is true then scheduler will switch to woken up thread (if it is runnable on the same core) - void OSThreadQueueInternal::wakeupSingleThreadWaitQueue(bool reschedule) + void OSThreadQueueInternal::wakeupSingleThreadWaitQueue(bool reschedule, bool sharedPriorityAndAffinityWorkaround) { cemu_assert_debug(__OSHasSchedulerLock()); OSThread_t* thread = takeFirstFromQueue(offsetof(OSThread_t, waitQueueLink)); @@ -159,7 +160,7 @@ namespace coreinit thread->state = OSThread_t::THREAD_STATE::STATE_READY; thread->currentWaitQueue = nullptr; coreinit::__OSAddReadyThreadToRunQueue(thread); - if (reschedule && thread->suspendCounter == 0 && PPCInterpreter_getCurrentInstance() && __OSCoreShouldSwitchToThread(coreinit::OSGetCurrentThread(), thread)) + if (reschedule && thread->suspendCounter == 0 && PPCInterpreter_getCurrentInstance() && __OSCoreShouldSwitchToThread(coreinit::OSGetCurrentThread(), thread, sharedPriorityAndAffinityWorkaround)) shouldReschedule = true; } if (shouldReschedule) From da8fd5b7c7ba51816d477e00f22d9a2cc56cb60b Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 29 May 2024 00:07:37 +0200 Subject: [PATCH 17/73] nn_save: Refactor and modernize code --- .../Interpreter/PPCInterpreterMain.cpp | 2 +- src/Cafe/HW/Espresso/PPCState.h | 2 +- src/Cafe/OS/libs/nn_save/nn_save.cpp | 1195 +++++------------ src/Common/MemPtr.h | 2 +- src/Common/StackAllocator.h | 9 +- 5 files changed, 323 insertions(+), 887 deletions(-) diff --git a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterMain.cpp b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterMain.cpp index a9ab49a5..ace1601f 100644 --- a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterMain.cpp +++ b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterMain.cpp @@ -90,7 +90,7 @@ uint8* PPCInterpreterGetStackPointer() return memory_getPointerFromVirtualOffset(PPCInterpreter_getCurrentInstance()->gpr[1]); } -uint8* PPCInterpreterGetAndModifyStackPointer(sint32 offset) +uint8* PPCInterpreter_PushAndReturnStackPointer(sint32 offset) { PPCInterpreter_t* hCPU = PPCInterpreter_getCurrentInstance(); uint8* result = memory_getPointerFromVirtualOffset(hCPU->gpr[1] - offset); diff --git a/src/Cafe/HW/Espresso/PPCState.h b/src/Cafe/HW/Espresso/PPCState.h index 134f73a8..c315ed0e 100644 --- a/src/Cafe/HW/Espresso/PPCState.h +++ b/src/Cafe/HW/Espresso/PPCState.h @@ -213,7 +213,7 @@ void PPCTimer_start(); // core info and control extern uint32 ppcThreadQuantum; -uint8* PPCInterpreterGetAndModifyStackPointer(sint32 offset); +uint8* PPCInterpreter_PushAndReturnStackPointer(sint32 offset); uint8* PPCInterpreterGetStackPointer(); void PPCInterpreterModifyStackPointer(sint32 offset); diff --git a/src/Cafe/OS/libs/nn_save/nn_save.cpp b/src/Cafe/OS/libs/nn_save/nn_save.cpp index 09d4413b..31f8ac8e 100644 --- a/src/Cafe/OS/libs/nn_save/nn_save.cpp +++ b/src/Cafe/OS/libs/nn_save/nn_save.cpp @@ -160,39 +160,60 @@ namespace save return FS_RESULT::FATAL_ERROR; } - typedef struct + struct AsyncResultData { - coreinit::OSEvent* event; - SAVEStatus returnStatus; + MEMPTR event; + betype returnStatus; + }; - MEMPTR thread; // own stuff until cond + event rewritten - } AsyncCallbackParam_t; + void SaveAsyncFinishCallback(PPCInterpreter_t* hCPU); - void AsyncCallback(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 returnStatus, void* p) + struct AsyncToSyncWrapper : public FSAsyncParams { - cemu_assert_debug(p && ((AsyncCallbackParam_t*)p)->event); + AsyncToSyncWrapper() + { + coreinit::OSInitEvent(&_event, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + ioMsgQueue = nullptr; + userContext = &_result; + userCallback = RPLLoader_MakePPCCallable(SaveAsyncFinishCallback); + _result.returnStatus = 0; + _result.event = &_event; + } - AsyncCallbackParam_t* param = (AsyncCallbackParam_t*)p; - param->returnStatus = returnStatus; - coreinit::OSSignalEvent(param->event); - } + ~AsyncToSyncWrapper() + { - void AsyncCallback(PPCInterpreter_t* hCPU) + } + + FSAsyncParams* GetAsyncParams() + { + return this; + } + + SAVEStatus GetResult() + { + return _result.returnStatus; + } + + void WaitForEvent() + { + coreinit::OSWaitEvent(&_event); + } + private: + coreinit::OSEvent _event; + AsyncResultData _result; + }; + + void SaveAsyncFinishCallback(PPCInterpreter_t* hCPU) { ppcDefineParamMEMPTR(client, coreinit::FSClient_t, 0); ppcDefineParamMEMPTR(block, coreinit::FSCmdBlock_t, 1); ppcDefineParamU32(returnStatus, 2); ppcDefineParamMEMPTR(userContext, void, 3); - MEMPTR param{ userContext }; - - // wait till thread is actually suspended - OSThread_t* thread = param->thread.GetPtr(); - while (thread->suspendCounter == 0 || thread->state == OSThread_t::THREAD_STATE::STATE_RUNNING) - coreinit::OSYieldThread(); - - param->returnStatus = returnStatus; - coreinit_resumeThread(param->thread.GetPtr(), 1000); + MEMPTR resultPtr{ userContext }; + resultPtr->returnStatus = returnStatus; + coreinit::OSSignalEvent(resultPtr->event); osLib_returnFromFunction(hCPU, 0); } @@ -320,18 +341,16 @@ namespace save return SAVE_STATUS_OK; } - SAVEStatus SAVERemoveAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) + SAVEStatus SAVEInitSaveDir(uint8 accountSlot) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - OSLockMutex(&g_nn_save->mutex); uint32 persistentId; if (GetPersistentIdEx(accountSlot, &persistentId)) { - char fullPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, path, fullPath)) - result = coreinit::FSRemoveAsync(client, block, (uint8*)fullPath, errHandling, (FSAsyncParams*)asyncParams); + acp::ACPStatus status = nn::acp::ACPCreateSaveDir(persistentId, iosu::acp::ACPDeviceType::InternalDeviceType); + result = ConvertACPToSaveStatus(status); } else result = (FSStatus)FS_RESULT::NOT_FOUND; @@ -340,27 +359,6 @@ namespace save return result; } - SAVEStatus SAVEMakeDirAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) - { - SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - - OSLockMutex(&g_nn_save->mutex); - - uint32 persistentId; - if (GetPersistentIdEx(accountSlot, &persistentId)) - { - char fullPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, path, fullPath)) - result = coreinit::FSMakeDirAsync(client, block, fullPath, errHandling, (FSAsyncParams*)asyncParams); - } - else - result = (FSStatus)FS_RESULT::NOT_FOUND; - - OSUnlockMutex(&g_nn_save->mutex); - - return result; - } - SAVEStatus SAVEOpenDirAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); @@ -383,6 +381,69 @@ namespace save return result; } + SAVEStatus SAVEOpenDir(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEOpenDirAsync(client, block, accountSlot, path, hDir, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + + SAVEStatus SAVEOpenDirOtherApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) + { + SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); + + OSLockMutex(&g_nn_save->mutex); + uint32 persistentId; + if (GetPersistentIdEx(accountSlot, &persistentId)) + { + char fullPath[SAVE_MAX_PATH_SIZE]; + if (GetAbsoluteFullPathOtherApplication(persistentId, titleId, path, fullPath) == FS_RESULT::SUCCESS) + result = coreinit::FSOpenDirAsync(client, block, fullPath, hDir, errHandling, (FSAsyncParams*)asyncParams); + } + else + result = (FSStatus)FS_RESULT::NOT_FOUND; + OSUnlockMutex(&g_nn_save->mutex); + + return result; + } + + SAVEStatus SAVEOpenDirOtherApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEOpenDirOtherApplicationAsync(client, block, titleId, accountSlot, path, hDir, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + + SAVEStatus SAVEOpenDirOtherNormalApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) + { + uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); + return SAVEOpenDirOtherApplicationAsync(client, block, titleId, accountSlot, path, hDir, errHandling, asyncParams); + } + + SAVEStatus SAVEOpenDirOtherNormalApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) + { + uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); + return SAVEOpenDirOtherApplication(client, block, titleId, accountSlot, path, hDir, errHandling); + } + + SAVEStatus SAVEOpenDirOtherNormalApplicationVariationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 variation, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) + { + uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID_VARIATION(uniqueId, variation); + return SAVEOpenDirOtherApplicationAsync(client, block, titleId, accountSlot, path, hDir, errHandling, asyncParams); + } + + SAVEStatus SAVEOpenDirOtherNormalApplicationVariation(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 variation, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) + { + uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID_VARIATION(uniqueId, variation); + return SAVEOpenDirOtherApplication(client, block, titleId, accountSlot, path, hDir, errHandling); + } + SAVEStatus SAVEOpenFileAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, const char* mode, FSFileHandlePtr outFileHandle, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); @@ -430,25 +491,12 @@ namespace save SAVEStatus SAVEOpenFileOtherApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, const char* mode, FSFileHandlePtr outFileHandle, FS_ERROR_MASK errHandling) { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = PPCInterpreter_makeCallableExportDepr(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = param.GetPointer(); - - SAVEStatus status = SAVEOpenFileOtherApplicationAsync(client, block, titleId, accountSlot, path, mode, outFileHandle, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; + StackAllocator asyncData; + SAVEStatus status = SAVEOpenFileOtherApplicationAsync(client, block, titleId, accountSlot, path, mode, outFileHandle, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); } SAVEStatus SAVEOpenFileOtherNormalApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, const char* mode, FSFileHandlePtr outFileHandle, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) @@ -475,27 +523,6 @@ namespace save return SAVEOpenFileOtherApplication(client, block, titleId, accountSlot, path, mode, outFileHandle, errHandling); } - SAVEStatus SAVEGetFreeSpaceSizeAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, FSLargeSize* freeSize, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) - { - SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - - OSLockMutex(&g_nn_save->mutex); - - uint32 persistentId; - if (GetPersistentIdEx(accountSlot, &persistentId)) - { - char fullPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, nullptr, fullPath)) - result = coreinit::FSGetFreeSpaceSizeAsync(client, block, fullPath, freeSize, errHandling, (FSAsyncParams*)asyncParams); - } - else - result = (FSStatus)FS_RESULT::NOT_FOUND; - - OSUnlockMutex(&g_nn_save->mutex); - - return result; - } - SAVEStatus SAVEGetStatAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); @@ -517,6 +544,16 @@ namespace save return result; } + SAVEStatus SAVEGetStat(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEGetStatAsync(client, block, accountSlot, path, stat, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + SAVEStatus SAVEGetStatOtherApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); @@ -538,12 +575,28 @@ namespace save return result; } + SAVEStatus SAVEGetStatOtherApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEGetStatOtherApplicationAsync(client, block, titleId, accountSlot, path, stat, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + SAVEStatus SAVEGetStatOtherNormalApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); return SAVEGetStatOtherApplicationAsync(client, block, titleId, accountSlot, path, stat, errHandling, asyncParams); } + SAVEStatus SAVEGetStatOtherNormalApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) + { + uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); + return SAVEGetStatOtherApplication(client, block, titleId, accountSlot, path, stat, errHandling); + } + SAVEStatus SAVEGetStatOtherDemoApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { uint64 titleId = SAVE_UNIQUE_DEMO_TO_TITLE_ID(uniqueId); @@ -562,644 +615,6 @@ namespace save return SAVEGetStatOtherApplicationAsync(client, block, titleId, accountSlot, path, stat, errHandling, asyncParams); } - SAVEStatus SAVEInitSaveDir(uint8 accountSlot) - { - SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - OSLockMutex(&g_nn_save->mutex); - - uint32 persistentId; - if (GetPersistentIdEx(accountSlot, &persistentId)) - { - acp::ACPStatus status = nn::acp::ACPCreateSaveDir(persistentId, iosu::acp::ACPDeviceType::InternalDeviceType); - result = ConvertACPToSaveStatus(status); - } - else - result = (FSStatus)FS_RESULT::NOT_FOUND; - - OSUnlockMutex(&g_nn_save->mutex); - return result; - } - - SAVEStatus SAVEGetFreeSpaceSize(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, FSLargeSize* freeSize, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEGetFreeSpaceSizeAsync(client, block, accountSlot, freeSize, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEGetFreeSpaceSize(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(returnedFreeSize, FSLargeSize, 3); - ppcDefineParamU32(errHandling, 4); - - const SAVEStatus result = SAVEGetFreeSpaceSize(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, returnedFreeSize.GetPtr(), errHandling); - cemuLog_log(LogType::Save, "SAVEGetFreeSpaceSize(0x{:08x}, 0x{:08x}, {:x}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - void export_SAVEGetFreeSpaceSizeAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(returnedFreeSize, FSLargeSize, 3); - ppcDefineParamU32(errHandling, 4); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 5); - - const SAVEStatus result = SAVEGetFreeSpaceSizeAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, returnedFreeSize.GetPtr(), errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEGetFreeSpaceSizeAsync(0x{:08x}, 0x{:08x}, {:x}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - void export_SAVEInit(PPCInterpreter_t* hCPU) - { - const SAVEStatus result = SAVEInit(); - cemuLog_log(LogType::Save, "SAVEInit() -> {:x}", result); - osLib_returnFromFunction(hCPU, result); - } - - void export_SAVERemoveAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamU32(errHandling, 4); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 5); - - const SAVEStatus result = SAVERemoveAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), errHandling, asyncParams.GetPtr()); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVERemove(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVERemoveAsync(client, block, accountSlot, path, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVERemove(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamU32(errHandling, 4); - - const SAVEStatus result = SAVERemove(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), errHandling); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVERenameAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* oldPath, const char* newPath, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) - { - SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - - OSLockMutex(&g_nn_save->mutex); - - uint32 persistentId; - if (GetPersistentIdEx(accountSlot, &persistentId)) - { - char fullOldPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, oldPath, fullOldPath)) - { - char fullNewPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, newPath, fullNewPath)) - result = coreinit::FSRenameAsync(client, block, fullOldPath, fullNewPath, errHandling, (FSAsyncParams*)asyncParams); - } - } - else - result = (FSStatus)FS_RESULT::NOT_FOUND; - - OSUnlockMutex(&g_nn_save->mutex); - - return result; - } - - void export_SAVERenameAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(oldPath, const char, 3); - ppcDefineParamMEMPTR(newPath, const char, 4); - ppcDefineParamU32(errHandling, 5); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 6); - - const SAVEStatus result = SAVERenameAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, oldPath.GetPtr(), newPath.GetPtr(), errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVERenameAsync(0x{:08x}, 0x{:08x}, {:x}, {}, {}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, oldPath.GetPtr(), newPath.GetPtr(), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVERename(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* oldPath, const char* newPath, FS_ERROR_MASK errHandling) - { - SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - - OSLockMutex(&g_nn_save->mutex); - uint32 persistentId; - if (GetPersistentIdEx(accountSlot, &persistentId)) - { - char fullOldPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, oldPath, fullOldPath)) - { - char fullNewPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPath(persistentId, newPath, fullNewPath)) - result = coreinit::FSRename(client, block, fullOldPath, fullNewPath, errHandling); - } - } - else - result = (FSStatus)FS_RESULT::NOT_FOUND; - OSUnlockMutex(&g_nn_save->mutex); - - return result; - } - - void export_SAVEOpenDirAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamMEMPTR(hDir, betype, 4); - ppcDefineParamU32(errHandling, 5); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 6); - - const SAVEStatus result = SAVEOpenDirAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), hDir, errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEOpenDirAsync(0x{:08x}, 0x{:08x}, {:x}, {}, 0x{:08x} ({:x}), {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), hDir.GetMPTR(), - (hDir.GetPtr() == nullptr ? 0 : (uint32)*hDir.GetPtr()), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDir(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEOpenDirAsync(client, block, accountSlot, path, hDir, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEOpenDir(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamMEMPTR(hDir, betype, 4); - ppcDefineParamU32(errHandling, 5); - - const SAVEStatus result = SAVEOpenDir(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), hDir, errHandling); - cemuLog_log(LogType::Save, "SAVEOpenDir(0x{:08x}, 0x{:08x}, {:x}, {}, 0x{:08x} ({:x}), {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), hDir.GetMPTR(), - (hDir.GetPtr() == nullptr ? 0 : (uint32)*hDir.GetPtr()), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDirOtherApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) - { - SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - - OSLockMutex(&g_nn_save->mutex); - uint32 persistentId; - if (GetPersistentIdEx(accountSlot, &persistentId)) - { - char fullPath[SAVE_MAX_PATH_SIZE]; - if (GetAbsoluteFullPathOtherApplication(persistentId, titleId, path, fullPath) == FS_RESULT::SUCCESS) - result = coreinit::FSOpenDirAsync(client, block, fullPath, hDir, errHandling, (FSAsyncParams*)asyncParams); - } - else - result = (FSStatus)FS_RESULT::NOT_FOUND; - OSUnlockMutex(&g_nn_save->mutex); - - return result; - } - - void export_SAVEOpenDirOtherApplicationAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU64(titleId, 2); - ppcDefineParamU8(accountSlot, 3); - ppcDefineParamMEMPTR(path, const char, 4); - ppcDefineParamMEMPTR(hDir, betype, 5); - ppcDefineParamU32(errHandling, 6); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 7); - - const SAVEStatus result = SAVEOpenDirOtherApplicationAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), titleId, accountSlot, path.GetPtr(), hDir, errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEOpenDirOtherApplicationAsync(0x{:08x}, 0x{:08x}, {:x}, {:x}, {}, 0x{:08x} ({:x}), {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), titleId, accountSlot, path.GetPtr(), hDir.GetMPTR(), - (hDir.GetPtr() == nullptr ? 0 : (uint32)*hDir.GetPtr()), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDirOtherApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEOpenDirOtherApplicationAsync(client, block, titleId, accountSlot, path, hDir, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEOpenDirOtherApplication(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU64(titleId, 2); - ppcDefineParamU8(accountSlot, 3); - ppcDefineParamMEMPTR(path, const char, 4); - ppcDefineParamMEMPTR(hDir, betype, 5); - ppcDefineParamU32(errHandling, 6); - - const SAVEStatus result = SAVEOpenDirOtherApplication(fsClient.GetPtr(), fsCmdBlock.GetPtr(), titleId, accountSlot, path.GetPtr(), hDir, errHandling); - cemuLog_log(LogType::Save, "SAVEOpenDirOtherApplication(0x{:08x}, 0x{:08x}, {:x}, {:x}, {}, 0x{:08x} ({:x}), {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), titleId, accountSlot, path.GetPtr(), hDir.GetMPTR(), - (hDir.GetPtr() == nullptr ? 0 : (uint32)*hDir.GetPtr()), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDirOtherNormalApplicationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) - { - uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); - return SAVEOpenDirOtherApplicationAsync(client, block, titleId, accountSlot, path, hDir, errHandling, asyncParams); - } - - void export_SAVEOpenDirOtherNormalApplicationAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(accountSlot, 3); - ppcDefineParamMEMPTR(path, const char, 4); - ppcDefineParamMEMPTR(hDir, betype, 5); - ppcDefineParamU32(errHandling, 6); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 7); - - const SAVEStatus result = SAVEOpenDirOtherNormalApplicationAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, accountSlot, path.GetPtr(), hDir, errHandling, asyncParams.GetPtr()); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDirOtherNormalApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) - { - uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); - return SAVEOpenDirOtherApplication(client, block, titleId, accountSlot, path, hDir, errHandling); - } - - void export_SAVEOpenDirOtherNormalApplication(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(accountSlot, 3); - ppcDefineParamMEMPTR(path, const char, 4); - ppcDefineParamMEMPTR(hDir, betype, 5); - ppcDefineParamU32(errHandling, 6); - - const SAVEStatus result = SAVEOpenDirOtherNormalApplication(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, accountSlot, path.GetPtr(), hDir, errHandling); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDirOtherNormalApplicationVariationAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 variation, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) - { - uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID_VARIATION(uniqueId, variation); - return SAVEOpenDirOtherApplicationAsync(client, block, titleId, accountSlot, path, hDir, errHandling, asyncParams); - } - - void export_SAVEOpenDirOtherNormalApplicationVariationAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(variation, 3); - ppcDefineParamU8(accountSlot, 4); - ppcDefineParamMEMPTR(path, const char, 5); - ppcDefineParamMEMPTR(hDir, betype, 6); - ppcDefineParamU32(errHandling, 7); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 8); - - const SAVEStatus result = SAVEOpenDirOtherNormalApplicationVariationAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, variation, accountSlot, path.GetPtr(), hDir, errHandling, asyncParams.GetPtr()); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenDirOtherNormalApplicationVariation(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 variation, uint8 accountSlot, const char* path, FSDirHandlePtr hDir, FS_ERROR_MASK errHandling) - { - uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID_VARIATION(uniqueId, variation); - return SAVEOpenDirOtherApplication(client, block, titleId, accountSlot, path, hDir, errHandling); - } - - void export_SAVEOpenDirOtherNormalApplicationVariation(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(variation, 3); - ppcDefineParamU8(accountSlot, 4); - ppcDefineParamMEMPTR(path, const char, 5); - ppcDefineParamMEMPTR(hDir, betype, 6); - ppcDefineParamU32(errHandling, 7); - - const SAVEStatus result = SAVEOpenDirOtherNormalApplicationVariation(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, variation, accountSlot, path.GetPtr(), hDir, errHandling); - osLib_returnFromFunction(hCPU, result); - } - - void export_SAVEMakeDirAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamU32(errHandling, 4); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 5); - - const SAVEStatus result = SAVEMakeDirAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEMakeDirAsync(0x{:08x}, 0x{:08x}, {:x}, {}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEMakeDir(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEMakeDirAsync(client, block, accountSlot, path, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEMakeDir(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamU32(errHandling, 4); - - const SAVEStatus result = SAVEMakeDir(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), errHandling); - cemuLog_log(LogType::Save, "SAVEMakeDir(0x{:08x}, 0x{:08x}, {:x}, {}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEOpenFile(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, const char* mode, FSFileHandlePtr outFileHandle, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = PPCInterpreter_makeCallableExportDepr(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = param.GetPointer(); - - SAVEStatus status = SAVEOpenFileAsync(client, block, accountSlot, path, mode, outFileHandle, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEInitSaveDir(PPCInterpreter_t* hCPU) - { - ppcDefineParamU8(accountSlot, 0); - const SAVEStatus result = SAVEInitSaveDir(accountSlot); - cemuLog_log(LogType::Save, "SAVEInitSaveDir({:x}) -> {:x}", accountSlot, result); - osLib_returnFromFunction(hCPU, result); - } - - void export_SAVEGetStatAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamMEMPTR(stat, FSStat_t, 4); - ppcDefineParamU32(errHandling, 5); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 6); - - const SAVEStatus result = SAVEGetStatAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), stat.GetPtr(), errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEGetStatAsync(0x{:08x}, 0x{:08x}, {:x}, {}, 0x{:08x}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), stat.GetMPTR(), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEGetStat(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEGetStatAsync(client, block, accountSlot, path, stat, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEGetStat(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamMEMPTR(stat, FSStat_t, 4); - ppcDefineParamU32(errHandling, 5); - - const SAVEStatus result = SAVEGetStat(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), stat.GetPtr(), errHandling); - cemuLog_log(LogType::Save, "SAVEGetStat(0x{:08x}, 0x{:08x}, {:x}, {}, 0x{:08x}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), stat.GetMPTR(), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - - void export_SAVEGetStatOtherApplicationAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU64(titleId, 2); - ppcDefineParamU8(accountSlot, 4); - ppcDefineParamMEMPTR(path, const char, 5); - ppcDefineParamMEMPTR(stat, FSStat_t, 6); - ppcDefineParamU32(errHandling, 7); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 8); - - const SAVEStatus result = SAVEGetStatOtherApplicationAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), titleId, accountSlot, path.GetPtr(), stat.GetPtr(), errHandling, asyncParams.GetPtr()); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEGetStatOtherApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint64 titleId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) - { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEGetStatOtherApplicationAsync(client, block, titleId, accountSlot, path, stat, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEGetStatOtherApplication(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU64(titleId, 2); - ppcDefineParamU8(accountSlot, 4); - ppcDefineParamMEMPTR(path, const char, 5); - ppcDefineParamMEMPTR(stat, FSStat_t, 6); - ppcDefineParamU32(errHandling, 7); - - const SAVEStatus result = SAVEGetStatOtherApplication(fsClient.GetPtr(), fsCmdBlock.GetPtr(), titleId, accountSlot, path.GetPtr(), stat.GetPtr(), errHandling); - osLib_returnFromFunction(hCPU, result); - } - - - void export_SAVEGetStatOtherNormalApplicationAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(accountSlot, 3); - ppcDefineParamMEMPTR(path, const char, 4); - ppcDefineParamMEMPTR(stat, FSStat_t, 5); - ppcDefineParamU32(errHandling, 6); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 8); - - const SAVEStatus result = SAVEGetStatOtherNormalApplicationAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, accountSlot, path.GetPtr(), stat.GetPtr(), errHandling, asyncParams.GetPtr()); - osLib_returnFromFunction(hCPU, result); - } - - SAVEStatus SAVEGetStatOtherNormalApplication(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) - { - uint64 titleId = SAVE_UNIQUE_TO_TITLE_ID(uniqueId); - return SAVEGetStatOtherApplication(client, block, titleId, accountSlot, path, stat, errHandling); - } - - void export_SAVEGetStatOtherNormalApplication(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(accountSlot, 3); - ppcDefineParamMEMPTR(path, const char, 4); - ppcDefineParamMEMPTR(stat, FSStat_t, 5); - ppcDefineParamU32(errHandling, 6); - - const SAVEStatus result = SAVEGetStatOtherNormalApplication(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, accountSlot, path.GetPtr(), stat.GetPtr(), errHandling); - osLib_returnFromFunction(hCPU, result); - } - - - - void export_SAVEGetStatOtherNormalApplicationVariationAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(variation, 3); - ppcDefineParamU8(accountSlot, 4); - ppcDefineParamMEMPTR(path, const char, 5); - ppcDefineParamMEMPTR(stat, FSStat_t, 6); - ppcDefineParamU32(errHandling, 7); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 8); - - const SAVEStatus result = SAVEGetStatOtherNormalApplicationVariationAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, variation, accountSlot, path.GetPtr(), stat.GetPtr(), errHandling, asyncParams.GetPtr()); - osLib_returnFromFunction(hCPU, result); - } - SAVEStatus SAVEGetStatOtherNormalApplicationVariation(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint32 uniqueId, uint8 variation, uint8 accountSlot, const char* path, FSStat_t* stat, FS_ERROR_MASK errHandling) { //peterBreak(); @@ -1208,21 +623,140 @@ namespace save return SAVEGetStatOtherApplication(client, block, titleId, accountSlot, path, stat, errHandling); } - void export_SAVEGetStatOtherNormalApplicationVariation(PPCInterpreter_t* hCPU) + SAVEStatus SAVEGetFreeSpaceSizeAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, FSLargeSize* freeSize, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU32(uniqueId, 2); - ppcDefineParamU8(variation, 3); - ppcDefineParamU8(accountSlot, 4); - ppcDefineParamMEMPTR(path, const char, 5); - ppcDefineParamMEMPTR(stat, FSStat_t, 6); - ppcDefineParamU32(errHandling, 7); + SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); - const SAVEStatus result = SAVEGetStatOtherNormalApplicationVariation(fsClient.GetPtr(), fsCmdBlock.GetPtr(), uniqueId, variation, accountSlot, path.GetPtr(), stat.GetPtr(), errHandling); - osLib_returnFromFunction(hCPU, result); + OSLockMutex(&g_nn_save->mutex); + + uint32 persistentId; + if (GetPersistentIdEx(accountSlot, &persistentId)) + { + char fullPath[SAVE_MAX_PATH_SIZE]; + if (GetAbsoluteFullPath(persistentId, nullptr, fullPath)) + result = coreinit::FSGetFreeSpaceSizeAsync(client, block, fullPath, freeSize, errHandling, (FSAsyncParams*)asyncParams); + } + else + result = (FSStatus)FS_RESULT::NOT_FOUND; + + OSUnlockMutex(&g_nn_save->mutex); + + return result; } + SAVEStatus SAVEGetFreeSpaceSize(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, FSLargeSize* freeSize, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEGetFreeSpaceSizeAsync(client, block, accountSlot, freeSize, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + + SAVEStatus SAVERemoveAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) + { + SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); + + OSLockMutex(&g_nn_save->mutex); + + uint32 persistentId; + if (GetPersistentIdEx(accountSlot, &persistentId)) + { + char fullPath[SAVE_MAX_PATH_SIZE]; + if (GetAbsoluteFullPath(persistentId, path, fullPath)) + result = coreinit::FSRemoveAsync(client, block, (uint8*)fullPath, errHandling, (FSAsyncParams*)asyncParams); + } + else + result = (FSStatus)FS_RESULT::NOT_FOUND; + + OSUnlockMutex(&g_nn_save->mutex); + return result; + } + + SAVEStatus SAVERemove(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVERemoveAsync(client, block, accountSlot, path, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + + SAVEStatus SAVERenameAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* oldPath, const char* newPath, FS_ERROR_MASK errHandling, FSAsyncParams* asyncParams) + { + SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); + OSLockMutex(&g_nn_save->mutex); + + uint32 persistentId; + if (GetPersistentIdEx(accountSlot, &persistentId)) + { + char fullOldPath[SAVE_MAX_PATH_SIZE]; + if (GetAbsoluteFullPath(persistentId, oldPath, fullOldPath)) + { + char fullNewPath[SAVE_MAX_PATH_SIZE]; + if (GetAbsoluteFullPath(persistentId, newPath, fullNewPath)) + result = coreinit::FSRenameAsync(client, block, fullOldPath, fullNewPath, errHandling, asyncParams); + } + } + else + result = (FSStatus)FS_RESULT::NOT_FOUND; + + OSUnlockMutex(&g_nn_save->mutex); + return result; + } + + SAVEStatus SAVERename(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* oldPath, const char* newPath, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVERenameAsync(client, block, accountSlot, oldPath, newPath, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + + SAVEStatus SAVEMakeDirAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) + { + SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); + + OSLockMutex(&g_nn_save->mutex); + + uint32 persistentId; + if (GetPersistentIdEx(accountSlot, &persistentId)) + { + char fullPath[SAVE_MAX_PATH_SIZE]; + if (GetAbsoluteFullPath(persistentId, path, fullPath)) + result = coreinit::FSMakeDirAsync(client, block, fullPath, errHandling, (FSAsyncParams*)asyncParams); + } + else + result = (FSStatus)FS_RESULT::NOT_FOUND; + + OSUnlockMutex(&g_nn_save->mutex); + + return result; + } + + SAVEStatus SAVEMakeDir(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEMakeDirAsync(client, block, accountSlot, path, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } + + SAVEStatus SAVEOpenFile(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, const char* mode, FSFileHandlePtr outFileHandle, FS_ERROR_MASK errHandling) + { + StackAllocator asyncData; + SAVEStatus status = SAVEOpenFileAsync(client, block, accountSlot, path, mode, outFileHandle, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); + } SAVEStatus SAVEGetSharedDataTitlePath(uint64 titleId, const char* dataFileName, char* output, sint32 outputLength) { @@ -1234,17 +768,6 @@ namespace save return result; } - void export_SAVEGetSharedDataTitlePath(PPCInterpreter_t* hCPU) - { - ppcDefineParamU64(titleId, 0); - ppcDefineParamMEMPTR(dataFileName, const char, 2); - ppcDefineParamMEMPTR(output, char, 3); - ppcDefineParamS32(outputLength, 4); - const SAVEStatus result = SAVEGetSharedDataTitlePath(titleId, dataFileName.GetPtr(), output.GetPtr(), outputLength); - cemuLog_log(LogType::Save, "SAVEGetSharedDataTitlePath(0x{:x}, {}, {}, 0x{:x}) -> {:x}", titleId, dataFileName.GetPtr(), output.GetPtr(), outputLength, result); - osLib_returnFromFunction(hCPU, result); - } - SAVEStatus SAVEGetSharedSaveDataPath(uint64 titleId, const char* dataFileName, char* output, uint32 outputLength) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); @@ -1255,16 +778,6 @@ namespace save return result; } - void export_SAVEGetSharedSaveDataPath(PPCInterpreter_t* hCPU) - { - ppcDefineParamU64(titleId, 0); - ppcDefineParamMEMPTR(dataFileName, const char, 2); - ppcDefineParamMEMPTR(output, char, 3); - ppcDefineParamU32(outputLength, 4); - const SAVEStatus result = SAVEGetSharedSaveDataPath(titleId, dataFileName.GetPtr(), output.GetPtr(), outputLength); - osLib_returnFromFunction(hCPU, result); - } - SAVEStatus SAVEChangeDirAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) { SAVEStatus result = (FSStatus)(FS_RESULT::FATAL_ERROR); @@ -1284,52 +797,14 @@ namespace save return result; } - void export_SAVEChangeDirAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamU32(errHandling, 4); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 5); - const SAVEStatus result = SAVEChangeDirAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEChangeDirAsync(0x{:08x}, 0x{:08x}, {:x}, {}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - SAVEStatus SAVEChangeDir(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, const char* path, FS_ERROR_MASK errHandling) { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEChangeDirAsync(client, block, accountSlot, path, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - - return status; - } - - void export_SAVEChangeDir(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamMEMPTR(path, const char, 3); - ppcDefineParamU32(errHandling, 4); - const SAVEStatus result = SAVEChangeDir(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, path.GetPtr(), errHandling); - cemuLog_log(LogType::Save, "SAVEChangeDir(0x{:08x}, 0x{:08x}, {:x}, {}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, path.GetPtr(), errHandling, result); - osLib_returnFromFunction(hCPU, result); + StackAllocator asyncData; + SAVEStatus status = SAVEChangeDirAsync(client, block, accountSlot, path, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); } SAVEStatus SAVEFlushQuotaAsync(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, FS_ERROR_MASK errHandling, const FSAsyncParams* asyncParams) @@ -1355,71 +830,45 @@ namespace save return result; } - void export_SAVEFlushQuotaAsync(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamU32(errHandling, 3); - ppcDefineParamMEMPTR(asyncParams, FSAsyncParams, 4); - const SAVEStatus result = SAVEFlushQuotaAsync(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, errHandling, asyncParams.GetPtr()); - cemuLog_log(LogType::Save, "SAVEFlushQuotaAsync(0x{:08x}, 0x{:08x}, {:x}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, errHandling, result); - osLib_returnFromFunction(hCPU, result); - } - SAVEStatus SAVEFlushQuota(coreinit::FSClient_t* client, coreinit::FSCmdBlock_t* block, uint8 accountSlot, FS_ERROR_MASK errHandling) { - MEMPTR currentThread{coreinit::OSGetCurrentThread()}; - FSAsyncParams asyncParams; - asyncParams.ioMsgQueue = nullptr; - asyncParams.userCallback = RPLLoader_MakePPCCallable(AsyncCallback); - - StackAllocator param; - param->thread = currentThread; - param->returnStatus = (FSStatus)FS_RESULT::SUCCESS; - asyncParams.userContext = ¶m; - - SAVEStatus status = SAVEFlushQuotaAsync(client, block, accountSlot, errHandling, &asyncParams); - if (status == (FSStatus)FS_RESULT::SUCCESS) - { - coreinit_suspendThread(currentThread, 1000); - PPCCore_switchToScheduler(); - return param->returnStatus; - } - return status; - } - - void export_SAVEFlushQuota(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 0); - ppcDefineParamMEMPTR(fsCmdBlock, coreinit::FSCmdBlock_t, 1); - ppcDefineParamU8(accountSlot, 2); - ppcDefineParamU32(errHandling, 3); - const SAVEStatus result = SAVEFlushQuota(fsClient.GetPtr(), fsCmdBlock.GetPtr(), accountSlot, errHandling); - cemuLog_log(LogType::Save, "SAVEFlushQuota(0x{:08x}, 0x{:08x}, {:x}, {:x}) -> {:x}", fsClient.GetMPTR(), fsCmdBlock.GetMPTR(), accountSlot, errHandling, result); - osLib_returnFromFunction(hCPU, result); + StackAllocator asyncData; + SAVEStatus status = SAVEFlushQuotaAsync(client, block, accountSlot, errHandling, asyncData->GetAsyncParams()); + if (status != (FSStatus)FS_RESULT::SUCCESS) + return status; + asyncData->WaitForEvent(); + return asyncData->GetResult(); } void load() { + cafeExportRegister("nn_save", SAVEInit, LogType::Save); + cafeExportRegister("nn_save", SAVEInitSaveDir, LogType::Save); + cafeExportRegister("nn_save", SAVEGetSharedDataTitlePath, LogType::Save); + cafeExportRegister("nn_save", SAVEGetSharedSaveDataPath, LogType::Save); - osLib_addFunction("nn_save", "SAVEInit", export_SAVEInit); - osLib_addFunction("nn_save", "SAVEInitSaveDir", export_SAVEInitSaveDir); - osLib_addFunction("nn_save", "SAVEGetSharedDataTitlePath", export_SAVEGetSharedDataTitlePath); - osLib_addFunction("nn_save", "SAVEGetSharedSaveDataPath", export_SAVEGetSharedSaveDataPath); + cafeExportRegister("nn_save", SAVEGetFreeSpaceSize, LogType::Save); + cafeExportRegister("nn_save", SAVEGetFreeSpaceSizeAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEMakeDir, LogType::Save); + cafeExportRegister("nn_save", SAVEMakeDirAsync, LogType::Save); + cafeExportRegister("nn_save", SAVERemove, LogType::Save); + cafeExportRegister("nn_save", SAVERemoveAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEChangeDir, LogType::Save); + cafeExportRegister("nn_save", SAVEChangeDirAsync, LogType::Save); + cafeExportRegister("nn_save", SAVERename, LogType::Save); + cafeExportRegister("nn_save", SAVERenameAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEFlushQuota, LogType::Save); + cafeExportRegister("nn_save", SAVEFlushQuotaAsync, LogType::Save); - // sync functions - osLib_addFunction("nn_save", "SAVEGetFreeSpaceSize", export_SAVEGetFreeSpaceSize); - osLib_addFunction("nn_save", "SAVEMakeDir", export_SAVEMakeDir); - osLib_addFunction("nn_save", "SAVERemove", export_SAVERemove); - osLib_addFunction("nn_save", "SAVEChangeDir", export_SAVEChangeDir); - osLib_addFunction("nn_save", "SAVEFlushQuota", export_SAVEFlushQuota); - - osLib_addFunction("nn_save", "SAVEGetStat", export_SAVEGetStat); - osLib_addFunction("nn_save", "SAVEGetStatOtherApplication", export_SAVEGetStatOtherApplication); - osLib_addFunction("nn_save", "SAVEGetStatOtherNormalApplication", export_SAVEGetStatOtherNormalApplication); - osLib_addFunction("nn_save", "SAVEGetStatOtherNormalApplicationVariation", export_SAVEGetStatOtherNormalApplicationVariation); + cafeExportRegister("nn_save", SAVEGetStat, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatOtherApplication, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatOtherApplicationAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatOtherNormalApplication, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatOtherNormalApplicationAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatOtherNormalApplicationVariation, LogType::Save); + cafeExportRegister("nn_save", SAVEGetStatOtherNormalApplicationVariationAsync, LogType::Save); cafeExportRegister("nn_save", SAVEOpenFile, LogType::Save); cafeExportRegister("nn_save", SAVEOpenFileAsync, LogType::Save); @@ -1430,30 +879,14 @@ namespace save cafeExportRegister("nn_save", SAVEOpenFileOtherNormalApplicationVariation, LogType::Save); cafeExportRegister("nn_save", SAVEOpenFileOtherNormalApplicationVariationAsync, LogType::Save); - osLib_addFunction("nn_save", "SAVEOpenDir", export_SAVEOpenDir); - osLib_addFunction("nn_save", "SAVEOpenDirOtherApplication", export_SAVEOpenDirOtherApplication); - osLib_addFunction("nn_save", "SAVEOpenDirOtherNormalApplication", export_SAVEOpenDirOtherNormalApplication); - osLib_addFunction("nn_save", "SAVEOpenDirOtherNormalApplicationVariation", export_SAVEOpenDirOtherNormalApplicationVariation); - - // async functions - osLib_addFunction("nn_save", "SAVEGetFreeSpaceSizeAsync", export_SAVEGetFreeSpaceSizeAsync); - osLib_addFunction("nn_save", "SAVEMakeDirAsync", export_SAVEMakeDirAsync); - osLib_addFunction("nn_save", "SAVERemoveAsync", export_SAVERemoveAsync); - osLib_addFunction("nn_save", "SAVERenameAsync", export_SAVERenameAsync); - cafeExportRegister("nn_save", SAVERename, LogType::Save); - - osLib_addFunction("nn_save", "SAVEChangeDirAsync", export_SAVEChangeDirAsync); - osLib_addFunction("nn_save", "SAVEFlushQuotaAsync", export_SAVEFlushQuotaAsync); - - osLib_addFunction("nn_save", "SAVEGetStatAsync", export_SAVEGetStatAsync); - osLib_addFunction("nn_save", "SAVEGetStatOtherApplicationAsync", export_SAVEGetStatOtherApplicationAsync); - osLib_addFunction("nn_save", "SAVEGetStatOtherNormalApplicationAsync", export_SAVEGetStatOtherNormalApplicationAsync); - osLib_addFunction("nn_save", "SAVEGetStatOtherNormalApplicationVariationAsync", export_SAVEGetStatOtherNormalApplicationVariationAsync); - - osLib_addFunction("nn_save", "SAVEOpenDirAsync", export_SAVEOpenDirAsync); - osLib_addFunction("nn_save", "SAVEOpenDirOtherApplicationAsync", export_SAVEOpenDirOtherApplicationAsync); - osLib_addFunction("nn_save", "SAVEOpenDirOtherNormalApplicationAsync", export_SAVEOpenDirOtherNormalApplicationAsync); - osLib_addFunction("nn_save", "SAVEOpenDirOtherNormalApplicationVariationAsync", export_SAVEOpenDirOtherNormalApplicationVariationAsync); + cafeExportRegister("nn_save", SAVEOpenDir, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirOtherApplication, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirOtherApplicationAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirOtherNormalApplication, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirOtherNormalApplicationVariation, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirOtherNormalApplicationAsync, LogType::Save); + cafeExportRegister("nn_save", SAVEOpenDirOtherNormalApplicationVariationAsync, LogType::Save); } void ResetToDefaultState() diff --git a/src/Common/MemPtr.h b/src/Common/MemPtr.h index b2362d0b..5fb73479 100644 --- a/src/Common/MemPtr.h +++ b/src/Common/MemPtr.h @@ -11,7 +11,7 @@ using PAddr = uint32; // physical address extern uint8* memory_base; extern uint8* PPCInterpreterGetStackPointer(); -extern uint8* PPCInterpreterGetAndModifyStackPointer(sint32 offset); +extern uint8* PPCInterpreter_PushAndReturnStackPointer(sint32 offset); extern void PPCInterpreterModifyStackPointer(sint32 offset); class MEMPTRBase {}; diff --git a/src/Common/StackAllocator.h b/src/Common/StackAllocator.h index 1dc52d51..856224cf 100644 --- a/src/Common/StackAllocator.h +++ b/src/Common/StackAllocator.h @@ -10,14 +10,16 @@ public: explicit StackAllocator(const uint32 items) { + m_items = items; m_modified_size = count * sizeof(T) * items + kStaticMemOffset * 2; - - auto tmp = PPCInterpreterGetStackPointer(); - m_ptr = (T*)(PPCInterpreterGetAndModifyStackPointer(m_modified_size) + kStaticMemOffset); + m_modified_size = (m_modified_size/8+7) * 8; // pad to 8 bytes + m_ptr = new(PPCInterpreter_PushAndReturnStackPointer(m_modified_size) + kStaticMemOffset) T[count * items](); } ~StackAllocator() { + for (size_t i = 0; i < count * m_items; ++i) + m_ptr[i].~T(); PPCInterpreterModifyStackPointer(-m_modified_size); } @@ -64,4 +66,5 @@ private: T* m_ptr; sint32 m_modified_size; + uint32 m_items; }; \ No newline at end of file From f576269ed0e52f5487a1e8ad19a109b5d4214bf0 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 29 May 2024 00:34:11 +0200 Subject: [PATCH 18/73] Refactor legacy method of emulating thread events --- src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp | 18 --------- src/Cafe/OS/libs/coreinit/coreinit_Thread.h | 5 --- src/Cafe/OS/libs/nn_idbe/nn_idbe.cpp | 13 ++++--- src/Cafe/OS/libs/nsyshid/nsyshid.cpp | 38 +++++++++---------- 4 files changed, 26 insertions(+), 48 deletions(-) diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp index b53d04ed..2f3808b7 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp @@ -1608,21 +1608,3 @@ namespace coreinit } } - -void coreinit_suspendThread(OSThread_t* OSThreadBE, sint32 count) -{ - // for legacy source - OSThreadBE->suspendCounter += count; -} - -void coreinit_resumeThread(OSThread_t* OSThreadBE, sint32 count) -{ - __OSLockScheduler(); - coreinit::__OSResumeThreadInternal(OSThreadBE, count); - __OSUnlockScheduler(); -} - -OSThread_t* coreinitThread_getCurrentThreadDepr(PPCInterpreter_t* hCPU) -{ - return coreinit::__currentCoreThread[PPCInterpreter_getCoreIndex(hCPU)]; -} diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h index 8b144bd3..df787bf0 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h @@ -621,11 +621,6 @@ namespace coreinit #pragma pack() // deprecated / clean up required -void coreinit_suspendThread(OSThread_t* OSThreadBE, sint32 count = 1); -void coreinit_resumeThread(OSThread_t* OSThreadBE, sint32 count = 1); - -OSThread_t* coreinitThread_getCurrentThreadDepr(PPCInterpreter_t* hCPU); - extern MPTR activeThread[256]; extern sint32 activeThreadCount; diff --git a/src/Cafe/OS/libs/nn_idbe/nn_idbe.cpp b/src/Cafe/OS/libs/nn_idbe/nn_idbe.cpp index a69f32a3..eb0178fe 100644 --- a/src/Cafe/OS/libs/nn_idbe/nn_idbe.cpp +++ b/src/Cafe/OS/libs/nn_idbe/nn_idbe.cpp @@ -40,7 +40,7 @@ namespace nn static_assert(offsetof(nnIdbeEncryptedIcon_t, iconData) == 0x22, ""); static_assert(sizeof(nnIdbeEncryptedIcon_t) == 0x12082); - void asyncDownloadIconFile(uint64 titleId, nnIdbeEncryptedIcon_t* iconOut, OSThread_t* thread) + void asyncDownloadIconFile(uint64 titleId, nnIdbeEncryptedIcon_t* iconOut, coreinit::OSEvent* event) { std::vector idbeData = NAPI::IDBE_RequestRawEncrypted(ActiveSettings::GetNetworkService(), titleId); if (idbeData.size() != sizeof(nnIdbeEncryptedIcon_t)) @@ -48,11 +48,11 @@ namespace nn // icon does not exist or has the wrong size cemuLog_log(LogType::Force, "IDBE: Failed to retrieve icon for title {:016x}", titleId); memset(iconOut, 0, sizeof(nnIdbeEncryptedIcon_t)); - coreinit_resumeThread(thread); + coreinit::OSSignalEvent(event); return; } memcpy(iconOut, idbeData.data(), sizeof(nnIdbeEncryptedIcon_t)); - coreinit_resumeThread(thread); + coreinit::OSSignalEvent(event); } void export_DownloadIconFile(PPCInterpreter_t* hCPU) @@ -62,9 +62,10 @@ namespace nn ppcDefineParamU32(uknR7, 4); ppcDefineParamU32(uknR8, 5); - auto asyncTask = std::async(std::launch::async, asyncDownloadIconFile, titleId, encryptedIconData, coreinit::OSGetCurrentThread()); - coreinit::OSSuspendThread(coreinit::OSGetCurrentThread()); - PPCCore_switchToScheduler(); + StackAllocator event; + coreinit::OSInitEvent(&event, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + auto asyncTask = std::async(std::launch::async, asyncDownloadIconFile, titleId, encryptedIconData, &event); + coreinit::OSWaitEvent(&event); osLib_returnFromFunction(hCPU, 1); } diff --git a/src/Cafe/OS/libs/nsyshid/nsyshid.cpp b/src/Cafe/OS/libs/nsyshid/nsyshid.cpp index ba3e3b96..ff5c4f45 100644 --- a/src/Cafe/OS/libs/nsyshid/nsyshid.cpp +++ b/src/Cafe/OS/libs/nsyshid/nsyshid.cpp @@ -429,8 +429,7 @@ namespace nsyshid // handler for synchronous HIDSetReport transfers sint32 _hidSetReportSync(std::shared_ptr device, uint8* reportData, sint32 length, - uint8* originalData, - sint32 originalLength, OSThread_t* osThread) + uint8* originalData, sint32 originalLength, coreinit::OSEvent* event) { _debugPrintHex("_hidSetReportSync Begin", reportData, length); sint32 returnCode = 0; @@ -440,7 +439,7 @@ namespace nsyshid } free(reportData); cemuLog_logDebug(LogType::Force, "_hidSetReportSync end. returnCode: {}", returnCode); - coreinit_resumeThread(osThread, 1000); + coreinit::OSSignalEvent(event); return returnCode; } @@ -484,11 +483,12 @@ namespace nsyshid sint32 returnCode = 0; if (callbackFuncMPTR == MPTR_NULL) { + // synchronous + StackAllocator event; + coreinit::OSInitEvent(&event, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); std::future res = std::async(std::launch::async, &_hidSetReportSync, device, reportData, - paddedLength + 1, data, dataLength, - coreinitThread_getCurrentThreadDepr(hCPU)); - coreinit_suspendThread(coreinitThread_getCurrentThreadDepr(hCPU), 1000); - PPCCore_switchToScheduler(); + paddedLength + 1, data, dataLength, &event); + coreinit::OSWaitEvent(&event); returnCode = res.get(); } else @@ -557,10 +557,10 @@ namespace nsyshid sint32 _hidReadSync(std::shared_ptr device, uint8* data, sint32 maxLength, - OSThread_t* osThread) + coreinit::OSEvent* event) { sint32 returnCode = _hidReadInternalSync(device, data, maxLength); - coreinit_resumeThread(osThread, 1000); + coreinit::OSSignalEvent(event); return returnCode; } @@ -591,10 +591,10 @@ namespace nsyshid else { // synchronous transfer - std::future res = std::async(std::launch::async, &_hidReadSync, device, data, maxLength, - coreinitThread_getCurrentThreadDepr(hCPU)); - coreinit_suspendThread(coreinitThread_getCurrentThreadDepr(hCPU), 1000); - PPCCore_switchToScheduler(); + StackAllocator event; + coreinit::OSInitEvent(&event, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + std::future res = std::async(std::launch::async, &_hidReadSync, device, data, maxLength, &event); + coreinit::OSWaitEvent(&event); returnCode = res.get(); } @@ -654,10 +654,10 @@ namespace nsyshid sint32 _hidWriteSync(std::shared_ptr device, uint8* data, sint32 maxLength, - OSThread_t* osThread) + coreinit::OSEvent* event) { sint32 returnCode = _hidWriteInternalSync(device, data, maxLength); - coreinit_resumeThread(osThread, 1000); + coreinit::OSSignalEvent(event); return returnCode; } @@ -688,10 +688,10 @@ namespace nsyshid else { // synchronous transfer - std::future res = std::async(std::launch::async, &_hidWriteSync, device, data, maxLength, - coreinitThread_getCurrentThreadDepr(hCPU)); - coreinit_suspendThread(coreinitThread_getCurrentThreadDepr(hCPU), 1000); - PPCCore_switchToScheduler(); + StackAllocator event; + coreinit::OSInitEvent(&event, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + std::future res = std::async(std::launch::async, &_hidWriteSync, device, data, maxLength, &event); + coreinit::OSWaitEvent(&event); returnCode = res.get(); } From d33337d5394754a738989fe4736abc534cefe8cb Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Tue, 28 May 2024 23:36:12 +0100 Subject: [PATCH 19/73] Fix GamePad window size (#1224) --- src/gui/PadViewFrame.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/PadViewFrame.cpp b/src/gui/PadViewFrame.cpp index 744df323..f2da2ca7 100644 --- a/src/gui/PadViewFrame.cpp +++ b/src/gui/PadViewFrame.cpp @@ -72,7 +72,7 @@ void PadViewFrame::InitializeRenderCanvas() m_render_canvas = GLCanvas_Create(this, wxSize(854, 480), false); sizer->Add(m_render_canvas, 1, wxEXPAND, 0, nullptr); } - SetSizer(sizer); + SetSizerAndFit(sizer); Layout(); m_render_canvas->Bind(wxEVT_KEY_UP, &PadViewFrame::OnKeyUp, this); From 5f825a1fa8eacf87b18a5b5666080c7c0dd55926 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 2 Jun 2024 20:29:10 +0200 Subject: [PATCH 20/73] Latte: Always allow views with the same format as base texture Fixes crash/assert in VC N64 titles --- src/Cafe/HW/Latte/Core/LatteTexture.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Cafe/HW/Latte/Core/LatteTexture.cpp b/src/Cafe/HW/Latte/Core/LatteTexture.cpp index 3754fb19..d8852891 100644 --- a/src/Cafe/HW/Latte/Core/LatteTexture.cpp +++ b/src/Cafe/HW/Latte/Core/LatteTexture.cpp @@ -235,6 +235,9 @@ void LatteTexture_InitSliceAndMipInfo(LatteTexture* texture) // if this function returns false, textures will not be synchronized even if their data overlaps bool LatteTexture_IsFormatViewCompatible(Latte::E_GX2SURFFMT formatA, Latte::E_GX2SURFFMT formatB) { + if(formatA == formatB) + return true; // if the format is identical then compatibility must be guaranteed (otherwise we can't create the necessary default view of a texture) + // todo - find a better way to handle this for (sint32 swap = 0; swap < 2; swap++) { From 16070458edddb8bd73baf3cbb1678f5a8b2dbc5e Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:39:06 +0200 Subject: [PATCH 21/73] Logging: Restructure menu + allow toggeling APIErrors logtype The logtype "APIErrors" previously was always enabled. This option is intended to help homebrew developers notice mistakes in how they use CafeOS API. But some commercial games trigger these a lot and cause log.txt bloat (e.g. seen in XCX). Thus this commit changes it so that it's off by default and instead can be toggled if desired. Additionally in this commit: - COS module logging options are no longer translatable (our debug logging is fundamentally English) - Restructured the log menu and moved the logging options that are mainly of interest to Cemu devs into a separate submenu --- src/Cafe/OS/libs/gx2/GX2_Command.cpp | 8 ----- src/Cafe/OS/libs/gx2/GX2_Command.h | 4 +-- src/Cafe/OS/libs/gx2/GX2_ContextState.cpp | 3 +- src/Cafe/OS/libs/gx2/GX2_Shader.cpp | 2 +- src/Cemu/Logging/CemuLogging.cpp | 1 + src/Cemu/Logging/CemuLogging.h | 2 +- src/gui/MainWindow.cpp | 44 +++++++++++++---------- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Cafe/OS/libs/gx2/GX2_Command.cpp b/src/Cafe/OS/libs/gx2/GX2_Command.cpp index 804e3da0..ec96a4ff 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Command.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_Command.cpp @@ -154,14 +154,6 @@ namespace GX2 return gx2WriteGatherPipe.displayListStart[coreIndex] != MPTR_NULL; } - bool GX2WriteGather_isDisplayListActive() - { - uint32 coreIndex = coreinit::OSGetCoreId(); - if (gx2WriteGatherPipe.displayListStart[coreIndex] != MPTR_NULL) - return true; - return false; - } - uint32 GX2WriteGather_getReadWriteDistance() { uint32 coreIndex = sGX2MainCoreIndex; diff --git a/src/Cafe/OS/libs/gx2/GX2_Command.h b/src/Cafe/OS/libs/gx2/GX2_Command.h index 635680e0..51c04928 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Command.h +++ b/src/Cafe/OS/libs/gx2/GX2_Command.h @@ -84,8 +84,6 @@ inline void gx2WriteGather_submit(Targs... args) namespace GX2 { - - bool GX2WriteGather_isDisplayListActive(); uint32 GX2WriteGather_getReadWriteDistance(); void GX2WriteGather_checkAndInsertWrapAroundMark(); @@ -96,6 +94,8 @@ namespace GX2 void GX2CallDisplayList(MPTR addr, uint32 size); void GX2DirectCallDisplayList(void* addr, uint32 size); + bool GX2GetDisplayListWriteStatus(); + void GX2Init_writeGather(); void GX2CommandInit(); void GX2CommandResetToDefaultState(); diff --git a/src/Cafe/OS/libs/gx2/GX2_ContextState.cpp b/src/Cafe/OS/libs/gx2/GX2_ContextState.cpp index a4cfc93d..cf150b47 100644 --- a/src/Cafe/OS/libs/gx2/GX2_ContextState.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_ContextState.cpp @@ -291,8 +291,7 @@ void gx2Export_GX2SetDefaultState(PPCInterpreter_t* hCPU) void _GX2ContextCreateRestoreStateDL(GX2ContextState_t* gx2ContextState) { // begin display list - if (GX2::GX2WriteGather_isDisplayListActive()) - assert_dbg(); + cemu_assert_debug(!GX2::GX2GetDisplayListWriteStatus()); // must not already be writing to a display list GX2::GX2BeginDisplayList((void*)gx2ContextState->loadDL_buffer, sizeof(gx2ContextState->loadDL_buffer)); _GX2Context_WriteCmdRestoreState(gx2ContextState, 0); uint32 displayListSize = GX2::GX2EndDisplayList((void*)gx2ContextState->loadDL_buffer); diff --git a/src/Cafe/OS/libs/gx2/GX2_Shader.cpp b/src/Cafe/OS/libs/gx2/GX2_Shader.cpp index d004288b..dfbbfcff 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Shader.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_Shader.cpp @@ -426,7 +426,7 @@ namespace GX2 } if((aluRegisterOffset+sizeInU32s) > 0x400) { - cemuLog_logOnce(LogType::APIErrors, "GX2SetVertexUniformReg values are out of range (offset {} + size {} must be equal or smaller than 0x400)", aluRegisterOffset, sizeInU32s); + cemuLog_logOnce(LogType::APIErrors, "GX2SetVertexUniformReg values are out of range (offset {} + size {} must be equal or smaller than 1024)", aluRegisterOffset, sizeInU32s); } if( (sizeInU32s&3) != 0) { diff --git a/src/Cemu/Logging/CemuLogging.cpp b/src/Cemu/Logging/CemuLogging.cpp index e49ece94..5cde2a7f 100644 --- a/src/Cemu/Logging/CemuLogging.cpp +++ b/src/Cemu/Logging/CemuLogging.cpp @@ -36,6 +36,7 @@ struct _LogContext const std::map g_logging_window_mapping { {LogType::UnsupportedAPI, "Unsupported API calls"}, + {LogType::APIErrors, "Invalid API usage"}, {LogType::CoreinitLogging, "Coreinit Logging"}, {LogType::CoreinitFile, "Coreinit File-Access"}, {LogType::CoreinitThreadSync, "Coreinit Thread-Synchronization"}, diff --git a/src/Cemu/Logging/CemuLogging.h b/src/Cemu/Logging/CemuLogging.h index 5fd652b3..a671ce51 100644 --- a/src/Cemu/Logging/CemuLogging.h +++ b/src/Cemu/Logging/CemuLogging.h @@ -7,7 +7,7 @@ enum class LogType : sint32 // note: IDs must be in range 1-64 Force = 63, // always enabled Placeholder = 62, // always disabled - APIErrors = Force, // alias for Force. Logs bad parameters or other API usage mistakes or unintended errors in OS libs + APIErrors = 61, // Logs bad parameters or other API usage mistakes or unintended errors in OS libs. Intended for homebrew developers CoreinitFile = 0, GX2 = 1, diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 33e2cdc1..03c69a7f 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -2191,27 +2191,35 @@ void MainWindow::RecreateMenu() m_menuBar->Append(nfcMenu, _("&NFC")); m_nfcMenuSeparator0 = nullptr; // debug->logging submenu - wxMenu* debugLoggingMenu = new wxMenu; + wxMenu* debugLoggingMenu = new wxMenu(); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::UnsupportedAPI), _("&Unsupported API calls"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::UnsupportedAPI)); + debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::APIErrors), _("&Invalid API usage"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::APIErrors)); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitLogging), _("&Coreinit Logging (OSReport/OSConsole)"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitLogging)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitFile), _("&Coreinit File-Access API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitFile)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitThreadSync), _("&Coreinit Thread-Synchronization API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitThreadSync)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitMem), _("&Coreinit Memory API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitMem)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitMP), _("&Coreinit MP API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitMP)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitThread), _("&Coreinit Thread API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitThread)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_NFP), _("&NN NFP"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NN_NFP)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_FP), _("&NN FP"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NN_FP)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::PRUDP), _("&PRUDP (for NN FP)"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::PRUDP)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_BOSS), _("&NN BOSS"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NN_BOSS)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::GX2), _("&GX2 API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::GX2)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::SoundAPI), _("&Audio API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::SoundAPI)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::InputAPI), _("&Input API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::InputAPI)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Socket), _("&Socket API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Socket)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Save), _("&Save API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Save)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::H264), _("&H264 API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::H264)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NFC), _("&NFC API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NFC)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NTAG), _("&NTAG API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NTAG)); + debugLoggingMenu->AppendSeparator(); + + wxMenu* logCosModulesMenu = new wxMenu(); + logCosModulesMenu->AppendCheckItem(0, _("&Options below are for experts. Leave off if unsure"), wxEmptyString)->Enable(false); + logCosModulesMenu->AppendSeparator(); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitFile), _("coreinit File-Access API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitFile)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitThreadSync), _("coreinit Thread-Synchronization API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitThreadSync)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitMem), _("coreinit Memory API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitMem)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitMP), _("coreinit MP API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitMP)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitThread), _("coreinit Thread API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::CoreinitThread)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Save), _("nn_save API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Save)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_NFP), _("nn_nfp API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NN_NFP)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_FP), _("nn_fp API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NN_FP)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::PRUDP), _("nn_fp PRUDP"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::PRUDP)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_BOSS), _("nn_boss API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NN_BOSS)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NFC), _("nfc API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NFC)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NTAG), _("ntag API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::NTAG)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Socket), _("nsysnet API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Socket)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::H264), _("h264 API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::H264)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::GX2), _("gx2 API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::GX2)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::SoundAPI), _("Audio API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::SoundAPI)); + logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::InputAPI), _("Input API"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::InputAPI)); + + debugLoggingMenu->AppendSubMenu(logCosModulesMenu, _("&CafeOS modules logging")); debugLoggingMenu->AppendSeparator(); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Patches), _("&Graphic pack patches"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::Patches)); debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::TextureCache), _("&Texture cache warnings"), wxEmptyString)->Check(cemuLog_isLoggingEnabled(LogType::TextureCache)); From 6772b1993ff678050d9a064dbd8c625eede272a1 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:34:42 +0200 Subject: [PATCH 22/73] vcpkg: Update dependencies (#1229) --- dependencies/vcpkg | 2 +- .../sdl2/alsa-dep-fix.patch | 13 -- .../vcpkg_overlay_ports/sdl2/deps.patch | 13 -- .../vcpkg_overlay_ports/sdl2/portfile.cmake | 137 ------------------ dependencies/vcpkg_overlay_ports/sdl2/usage | 8 - .../vcpkg_overlay_ports/sdl2/vcpkg.json | 68 --------- .../vcpkg_overlay_ports/tiff/FindCMath.patch | 13 -- .../vcpkg_overlay_ports/tiff/portfile.cmake | 86 ----------- dependencies/vcpkg_overlay_ports/tiff/usage | 9 -- .../tiff/vcpkg-cmake-wrapper.cmake.in | 104 ------------- .../vcpkg_overlay_ports/tiff/vcpkg.json | 67 --------- .../dbus/cmake.dep.patch | 15 -- .../dbus/getpeereid.patch | 26 ---- .../dbus/libsystemd.patch | 15 -- .../dbus/pkgconfig.patch | 21 --- .../dbus/portfile.cmake | 88 ----------- .../vcpkg_overlay_ports_linux/dbus/vcpkg.json | 30 ---- .../sdl2/alsa-dep-fix.patch | 13 -- .../vcpkg_overlay_ports_linux/sdl2/deps.patch | 13 -- .../sdl2/portfile.cmake | 137 ------------------ .../vcpkg_overlay_ports_linux/sdl2/usage | 8 - .../vcpkg_overlay_ports_linux/sdl2/vcpkg.json | 68 --------- .../tiff/FindCMath.patch | 13 -- .../tiff/portfile.cmake | 86 ----------- .../vcpkg_overlay_ports_linux/tiff/usage | 9 -- .../tiff/vcpkg-cmake-wrapper.cmake.in | 104 ------------- .../vcpkg_overlay_ports_linux/tiff/vcpkg.json | 67 --------- .../sdl2/alsa-dep-fix.patch | 13 -- .../vcpkg_overlay_ports_mac/sdl2/deps.patch | 13 -- .../sdl2/portfile.cmake | 137 ------------------ .../vcpkg_overlay_ports_mac/sdl2/usage | 8 - .../vcpkg_overlay_ports_mac/sdl2/vcpkg.json | 68 --------- .../tiff/FindCMath.patch | 13 -- .../tiff/portfile.cmake | 86 ----------- .../vcpkg_overlay_ports_mac/tiff/usage | 9 -- .../tiff/vcpkg-cmake-wrapper.cmake.in | 104 ------------- .../vcpkg_overlay_ports_mac/tiff/vcpkg.json | 67 --------- vcpkg.json | 18 ++- 38 files changed, 18 insertions(+), 1751 deletions(-) delete mode 100644 dependencies/vcpkg_overlay_ports/sdl2/alsa-dep-fix.patch delete mode 100644 dependencies/vcpkg_overlay_ports/sdl2/deps.patch delete mode 100644 dependencies/vcpkg_overlay_ports/sdl2/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports/sdl2/usage delete mode 100644 dependencies/vcpkg_overlay_ports/sdl2/vcpkg.json delete mode 100644 dependencies/vcpkg_overlay_ports/tiff/FindCMath.patch delete mode 100644 dependencies/vcpkg_overlay_ports/tiff/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports/tiff/usage delete mode 100644 dependencies/vcpkg_overlay_ports/tiff/vcpkg-cmake-wrapper.cmake.in delete mode 100644 dependencies/vcpkg_overlay_ports/tiff/vcpkg.json delete mode 100644 dependencies/vcpkg_overlay_ports_linux/dbus/cmake.dep.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/dbus/getpeereid.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/dbus/libsystemd.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/dbus/pkgconfig.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/dbus/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports_linux/dbus/vcpkg.json delete mode 100644 dependencies/vcpkg_overlay_ports_linux/sdl2/alsa-dep-fix.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/sdl2/deps.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/sdl2/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports_linux/sdl2/usage delete mode 100644 dependencies/vcpkg_overlay_ports_linux/sdl2/vcpkg.json delete mode 100644 dependencies/vcpkg_overlay_ports_linux/tiff/FindCMath.patch delete mode 100644 dependencies/vcpkg_overlay_ports_linux/tiff/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports_linux/tiff/usage delete mode 100644 dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg-cmake-wrapper.cmake.in delete mode 100644 dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg.json delete mode 100644 dependencies/vcpkg_overlay_ports_mac/sdl2/alsa-dep-fix.patch delete mode 100644 dependencies/vcpkg_overlay_ports_mac/sdl2/deps.patch delete mode 100644 dependencies/vcpkg_overlay_ports_mac/sdl2/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports_mac/sdl2/usage delete mode 100644 dependencies/vcpkg_overlay_ports_mac/sdl2/vcpkg.json delete mode 100644 dependencies/vcpkg_overlay_ports_mac/tiff/FindCMath.patch delete mode 100644 dependencies/vcpkg_overlay_ports_mac/tiff/portfile.cmake delete mode 100644 dependencies/vcpkg_overlay_ports_mac/tiff/usage delete mode 100644 dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg-cmake-wrapper.cmake.in delete mode 100644 dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg.json diff --git a/dependencies/vcpkg b/dependencies/vcpkg index cbf4a664..a4275b7e 160000 --- a/dependencies/vcpkg +++ b/dependencies/vcpkg @@ -1 +1 @@ -Subproject commit cbf4a6641528cee6f172328984576f51698de726 +Subproject commit a4275b7eee79fb24ec2e135481ef5fce8b41c339 diff --git a/dependencies/vcpkg_overlay_ports/sdl2/alsa-dep-fix.patch b/dependencies/vcpkg_overlay_ports/sdl2/alsa-dep-fix.patch deleted file mode 100644 index 5b2c77b9..00000000 --- a/dependencies/vcpkg_overlay_ports/sdl2/alsa-dep-fix.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/SDL2Config.cmake.in b/SDL2Config.cmake.in -index cc8bcf26d..ead829767 100644 ---- a/SDL2Config.cmake.in -+++ b/SDL2Config.cmake.in -@@ -35,7 +35,7 @@ include("${CMAKE_CURRENT_LIST_DIR}/sdlfind.cmake") - - set(SDL_ALSA @SDL_ALSA@) - set(SDL_ALSA_SHARED @SDL_ALSA_SHARED@) --if(SDL_ALSA AND NOT SDL_ALSA_SHARED AND TARGET SDL2::SDL2-static) -+if(SDL_ALSA) - sdlFindALSA() - endif() - unset(SDL_ALSA) diff --git a/dependencies/vcpkg_overlay_ports/sdl2/deps.patch b/dependencies/vcpkg_overlay_ports/sdl2/deps.patch deleted file mode 100644 index a8637d8c..00000000 --- a/dependencies/vcpkg_overlay_ports/sdl2/deps.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake -index 65a98efbe..2f99f28f1 100644 ---- a/cmake/sdlchecks.cmake -+++ b/cmake/sdlchecks.cmake -@@ -352,7 +352,7 @@ endmacro() - # - HAVE_SDL_LOADSO opt - macro(CheckLibSampleRate) - if(SDL_LIBSAMPLERATE) -- find_package(SampleRate QUIET) -+ find_package(SampleRate CONFIG REQUIRED) - if(SampleRate_FOUND AND TARGET SampleRate::samplerate) - set(HAVE_LIBSAMPLERATE TRUE) - set(HAVE_LIBSAMPLERATE_H TRUE) diff --git a/dependencies/vcpkg_overlay_ports/sdl2/portfile.cmake b/dependencies/vcpkg_overlay_ports/sdl2/portfile.cmake deleted file mode 100644 index 22685e6a..00000000 --- a/dependencies/vcpkg_overlay_ports/sdl2/portfile.cmake +++ /dev/null @@ -1,137 +0,0 @@ -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO libsdl-org/SDL - REF "release-${VERSION}" - SHA512 c7635a83a52f3970a372b804a8631f0a7e6b8d89aed1117bcc54a2040ad0928122175004cf2b42cf84a4fd0f86236f779229eaa63dfa6ca9c89517f999c5ff1c - HEAD_REF main - PATCHES - deps.patch - alsa-dep-fix.patch -) - -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "static" SDL_STATIC) -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" SDL_SHARED) -string(COMPARE EQUAL "${VCPKG_CRT_LINKAGE}" "static" FORCE_STATIC_VCRT) - -vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - alsa SDL_ALSA - alsa CMAKE_REQUIRE_FIND_PACKAGE_ALSA - ibus SDL_IBUS - samplerate SDL_LIBSAMPLERATE - vulkan SDL_VULKAN - wayland SDL_WAYLAND - x11 SDL_X11 - INVERTED_FEATURES - alsa CMAKE_DISABLE_FIND_PACKAGE_ALSA -) - -if ("x11" IN_LIST FEATURES) - message(WARNING "You will need to install Xorg dependencies to use feature x11:\nsudo apt install libx11-dev libxft-dev libxext-dev\n") -endif() -if ("wayland" IN_LIST FEATURES) - message(WARNING "You will need to install Wayland dependencies to use feature wayland:\nsudo apt install libwayland-dev libxkbcommon-dev libegl1-mesa-dev\n") -endif() -if ("ibus" IN_LIST FEATURES) - message(WARNING "You will need to install ibus dependencies to use feature ibus:\nsudo apt install libibus-1.0-dev\n") -endif() - -if(VCPKG_TARGET_IS_UWP) - set(configure_opts WINDOWS_USE_MSBUILD) -endif() - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - ${configure_opts} - OPTIONS ${FEATURE_OPTIONS} - -DSDL_STATIC=${SDL_STATIC} - -DSDL_SHARED=${SDL_SHARED} - -DSDL_FORCE_STATIC_VCRT=${FORCE_STATIC_VCRT} - -DSDL_LIBC=ON - -DSDL_TEST=OFF - -DSDL_INSTALL_CMAKEDIR="cmake" - -DCMAKE_DISABLE_FIND_PACKAGE_Git=ON - -DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=ON - -DSDL_LIBSAMPLERATE_SHARED=OFF - MAYBE_UNUSED_VARIABLES - SDL_FORCE_STATIC_VCRT - PKG_CONFIG_USE_CMAKE_PREFIX_PATH -) - -vcpkg_cmake_install() -vcpkg_cmake_config_fixup(CONFIG_PATH cmake) - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" - "${CURRENT_PACKAGES_DIR}/bin/sdl2-config" - "${CURRENT_PACKAGES_DIR}/debug/bin/sdl2-config" - "${CURRENT_PACKAGES_DIR}/SDL2.framework" - "${CURRENT_PACKAGES_DIR}/debug/SDL2.framework" - "${CURRENT_PACKAGES_DIR}/share/licenses" - "${CURRENT_PACKAGES_DIR}/share/aclocal" -) - -file(GLOB BINS "${CURRENT_PACKAGES_DIR}/debug/bin/*" "${CURRENT_PACKAGES_DIR}/bin/*") -if(NOT BINS) - file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/bin" - "${CURRENT_PACKAGES_DIR}/debug/bin" - ) -endif() - -if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_UWP AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/lib/manual-link") - file(RENAME "${CURRENT_PACKAGES_DIR}/lib/SDL2main.lib" "${CURRENT_PACKAGES_DIR}/lib/manual-link/SDL2main.lib") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link") - file(RENAME "${CURRENT_PACKAGES_DIR}/debug/lib/SDL2maind.lib" "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link/SDL2maind.lib") - endif() - - file(GLOB SHARE_FILES "${CURRENT_PACKAGES_DIR}/share/sdl2/*.cmake") - foreach(SHARE_FILE ${SHARE_FILES}) - vcpkg_replace_string("${SHARE_FILE}" "lib/SDL2main" "lib/manual-link/SDL2main") - endforeach() -endif() - -vcpkg_copy_pdbs() - -set(DYLIB_COMPATIBILITY_VERSION_REGEX "set\\(DYLIB_COMPATIBILITY_VERSION (.+)\\)") -set(DYLIB_CURRENT_VERSION_REGEX "set\\(DYLIB_CURRENT_VERSION (.+)\\)") -file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_COMPATIBILITY_VERSION REGEX ${DYLIB_COMPATIBILITY_VERSION_REGEX}) -file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_CURRENT_VERSION REGEX ${DYLIB_CURRENT_VERSION_REGEX}) -string(REGEX REPLACE ${DYLIB_COMPATIBILITY_VERSION_REGEX} "\\1" DYLIB_COMPATIBILITY_VERSION "${DYLIB_COMPATIBILITY_VERSION}") -string(REGEX REPLACE ${DYLIB_CURRENT_VERSION_REGEX} "\\1" DYLIB_CURRENT_VERSION "${DYLIB_CURRENT_VERSION}") - -if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2main" "-lSDL2maind") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2 " "-lSDL2d ") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-static " "-lSDL2-staticd ") -endif() - -if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic" AND VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-lSDL2-static " " ") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-staticd " " ") - endif() -endif() - -if(VCPKG_TARGET_IS_UWP) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "d") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") - endif() -endif() - -vcpkg_fixup_pkgconfig() - -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") diff --git a/dependencies/vcpkg_overlay_ports/sdl2/usage b/dependencies/vcpkg_overlay_ports/sdl2/usage deleted file mode 100644 index 1cddcd46..00000000 --- a/dependencies/vcpkg_overlay_ports/sdl2/usage +++ /dev/null @@ -1,8 +0,0 @@ -sdl2 provides CMake targets: - - find_package(SDL2 CONFIG REQUIRED) - target_link_libraries(main - PRIVATE - $ - $,SDL2::SDL2,SDL2::SDL2-static> - ) diff --git a/dependencies/vcpkg_overlay_ports/sdl2/vcpkg.json b/dependencies/vcpkg_overlay_ports/sdl2/vcpkg.json deleted file mode 100644 index 1f460375..00000000 --- a/dependencies/vcpkg_overlay_ports/sdl2/vcpkg.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "sdl2", - "version": "2.30.0", - "description": "Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.", - "homepage": "https://www.libsdl.org/download-2.0.php", - "license": "Zlib", - "dependencies": [ - { - "name": "dbus", - "default-features": false, - "platform": "linux" - }, - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - { - "name": "ibus", - "platform": "linux" - }, - { - "name": "wayland", - "platform": "linux" - }, - { - "name": "x11", - "platform": "linux" - } - ], - "features": { - "alsa": { - "description": "Support for alsa audio", - "dependencies": [ - { - "name": "alsa", - "platform": "linux" - } - ] - }, - "ibus": { - "description": "Build with ibus IME support", - "supports": "linux" - }, - "samplerate": { - "description": "Use libsamplerate for audio rate conversion", - "dependencies": [ - "libsamplerate" - ] - }, - "vulkan": { - "description": "Vulkan functionality for SDL" - }, - "wayland": { - "description": "Build with Wayland support", - "supports": "linux" - }, - "x11": { - "description": "Build with X11 support", - "supports": "!windows" - } - } -} diff --git a/dependencies/vcpkg_overlay_ports/tiff/FindCMath.patch b/dependencies/vcpkg_overlay_ports/tiff/FindCMath.patch deleted file mode 100644 index 70654cf8..00000000 --- a/dependencies/vcpkg_overlay_ports/tiff/FindCMath.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/FindCMath.cmake b/cmake/FindCMath.cmake -index ad92218..dd42aba 100644 ---- a/cmake/FindCMath.cmake -+++ b/cmake/FindCMath.cmake -@@ -31,7 +31,7 @@ include(CheckSymbolExists) - include(CheckLibraryExists) - - check_symbol_exists(pow "math.h" CMath_HAVE_LIBC_POW) --find_library(CMath_LIBRARY NAMES m) -+find_library(CMath_LIBRARY NAMES m PATHS ${CMAKE_C_IMPLICIT_LINK_DIRECTORIES}) - - if(NOT CMath_HAVE_LIBC_POW) - set(CMAKE_REQUIRED_LIBRARIES_SAVE ${CMAKE_REQUIRED_LIBRARIES}) diff --git a/dependencies/vcpkg_overlay_ports/tiff/portfile.cmake b/dependencies/vcpkg_overlay_ports/tiff/portfile.cmake deleted file mode 100644 index 426d8af7..00000000 --- a/dependencies/vcpkg_overlay_ports/tiff/portfile.cmake +++ /dev/null @@ -1,86 +0,0 @@ -vcpkg_from_gitlab( - GITLAB_URL https://gitlab.com - OUT_SOURCE_PATH SOURCE_PATH - REPO libtiff/libtiff - REF "v${VERSION}" - SHA512 ef2f1d424219d9e245069b7d23e78f5e817cf6ee516d46694915ab6c8909522166f84997513d20a702f4e52c3f18467813935b328fafa34bea5156dee00f66fa - HEAD_REF master - PATCHES - FindCMath.patch -) - -vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - cxx cxx - jpeg jpeg - jpeg CMAKE_REQUIRE_FIND_PACKAGE_JPEG - libdeflate libdeflate - libdeflate CMAKE_REQUIRE_FIND_PACKAGE_Deflate - lzma lzma - lzma CMAKE_REQUIRE_FIND_PACKAGE_liblzma - tools tiff-tools - webp webp - webp CMAKE_REQUIRE_FIND_PACKAGE_WebP - zip zlib - zip CMAKE_REQUIRE_FIND_PACKAGE_ZLIB - zstd zstd - zstd CMAKE_REQUIRE_FIND_PACKAGE_ZSTD -) - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - OPTIONS - ${FEATURE_OPTIONS} - -DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON - -Dtiff-docs=OFF - -Dtiff-contrib=OFF - -Dtiff-tests=OFF - -Djbig=OFF # This is disabled by default due to GPL/Proprietary licensing. - -Djpeg12=OFF - -Dlerc=OFF - -DCMAKE_DISABLE_FIND_PACKAGE_OpenGL=ON - -DCMAKE_DISABLE_FIND_PACKAGE_GLUT=ON - -DZSTD_HAVE_DECOMPRESS_STREAM=ON - -DHAVE_JPEGTURBO_DUAL_MODE_8_12=OFF - OPTIONS_DEBUG - -DCMAKE_DEBUG_POSTFIX=d # tiff sets "d" for MSVC only. - MAYBE_UNUSED_VARIABLES - CMAKE_DISABLE_FIND_PACKAGE_GLUT - CMAKE_DISABLE_FIND_PACKAGE_OpenGL - ZSTD_HAVE_DECOMPRESS_STREAM -) - -vcpkg_cmake_install() - -# CMake config wasn't packaged in the past and is not yet usable now, -# cf. https://gitlab.com/libtiff/libtiff/-/merge_requests/496 -# vcpkg_cmake_config_fixup(CONFIG_PATH "lib/cmake/tiff") -file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/lib/cmake" "${CURRENT_PACKAGES_DIR}/debug/lib/cmake") - -set(_file "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libtiff-4.pc") -if(EXISTS "${_file}") - vcpkg_replace_string("${_file}" "-ltiff" "-ltiffd") -endif() -vcpkg_fixup_pkgconfig() - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" -) - -configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake.in" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-cmake-wrapper.cmake" @ONLY) - -if ("tools" IN_LIST FEATURES) - vcpkg_copy_tools(TOOL_NAMES - tiffcp - tiffdump - tiffinfo - tiffset - tiffsplit - AUTO_CLEAN - ) -endif() - -vcpkg_copy_pdbs() -file(COPY "${CURRENT_PORT_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") diff --git a/dependencies/vcpkg_overlay_ports/tiff/usage b/dependencies/vcpkg_overlay_ports/tiff/usage deleted file mode 100644 index d47265b1..00000000 --- a/dependencies/vcpkg_overlay_ports/tiff/usage +++ /dev/null @@ -1,9 +0,0 @@ -tiff is compatible with built-in CMake targets: - - find_package(TIFF REQUIRED) - target_link_libraries(main PRIVATE TIFF::TIFF) - -tiff provides pkg-config modules: - - # Tag Image File Format (TIFF) library. - libtiff-4 diff --git a/dependencies/vcpkg_overlay_ports/tiff/vcpkg-cmake-wrapper.cmake.in b/dependencies/vcpkg_overlay_ports/tiff/vcpkg-cmake-wrapper.cmake.in deleted file mode 100644 index 1d04ec7a..00000000 --- a/dependencies/vcpkg_overlay_ports/tiff/vcpkg-cmake-wrapper.cmake.in +++ /dev/null @@ -1,104 +0,0 @@ -cmake_policy(PUSH) -cmake_policy(SET CMP0012 NEW) -cmake_policy(SET CMP0057 NEW) -set(z_vcpkg_tiff_find_options "") -if("REQUIRED" IN_LIST ARGS) - list(APPEND z_vcpkg_tiff_find_options "REQUIRED") -endif() -if("QUIET" IN_LIST ARGS) - list(APPEND z_vcpkg_tiff_find_options "QUIET") -endif() - -_find_package(${ARGS}) - -if(TIFF_FOUND AND "@VCPKG_LIBRARY_LINKAGE@" STREQUAL "static") - include(SelectLibraryConfigurations) - set(z_vcpkg_tiff_link_libraries "") - set(z_vcpkg_tiff_libraries "") - if("@webp@") - find_package(WebP CONFIG ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${WebP_LIBRARIES}) - endif() - if("@lzma@") - find_package(LibLZMA ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${LIBLZMA_LIBRARIES}) - endif() - if("@jpeg@") - find_package(JPEG ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${JPEG_LIBRARIES}) - endif() - if("@zstd@") - find_package(zstd CONFIG ${z_vcpkg_tiff_find_options}) - set(z_vcpkg_tiff_zstd_target_property "IMPORTED_LOCATION_") - if(TARGET zstd::libzstd_shared) - set(z_vcpkg_tiff_zstd "\$") - set(z_vcpkg_tiff_zstd_target zstd::libzstd_shared) - if(WIN32) - set(z_vcpkg_tiff_zstd_target_property "IMPORTED_IMPLIB_") - endif() - else() - set(z_vcpkg_tiff_zstd "\$") - set(z_vcpkg_tiff_zstd_target zstd::libzstd_static) - endif() - get_target_property(z_vcpkg_tiff_zstd_configs "${z_vcpkg_tiff_zstd_target}" IMPORTED_CONFIGURATIONS) - foreach(z_vcpkg_config IN LISTS z_vcpkg_tiff_zstd_configs) - get_target_property(ZSTD_LIBRARY_${z_vcpkg_config} "${z_vcpkg_tiff_zstd_target}" "${z_vcpkg_tiff_zstd_target_property}${z_vcpkg_config}") - endforeach() - select_library_configurations(ZSTD) - if(NOT TARGET ZSTD::ZSTD) - add_library(ZSTD::ZSTD INTERFACE IMPORTED) - set_property(TARGET ZSTD::ZSTD APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${z_vcpkg_tiff_zstd}) - endif() - list(APPEND z_vcpkg_tiff_link_libraries ${z_vcpkg_tiff_zstd}) - list(APPEND z_vcpkg_tiff_libraries ${ZSTD_LIBRARIES}) - unset(z_vcpkg_tiff_zstd) - unset(z_vcpkg_tiff_zstd_configs) - unset(z_vcpkg_config) - unset(z_vcpkg_tiff_zstd_target) - endif() - if("@libdeflate@") - find_package(libdeflate ${z_vcpkg_tiff_find_options}) - set(z_vcpkg_property "IMPORTED_LOCATION_") - if(TARGET libdeflate::libdeflate_shared) - set(z_vcpkg_libdeflate_target libdeflate::libdeflate_shared) - if(WIN32) - set(z_vcpkg_property "IMPORTED_IMPLIB_") - endif() - else() - set(z_vcpkg_libdeflate_target libdeflate::libdeflate_static) - endif() - get_target_property(z_vcpkg_libdeflate_configs "${z_vcpkg_libdeflate_target}" IMPORTED_CONFIGURATIONS) - foreach(z_vcpkg_config IN LISTS z_vcpkg_libdeflate_configs) - get_target_property(Z_VCPKG_DEFLATE_LIBRARY_${z_vcpkg_config} "${z_vcpkg_libdeflate_target}" "${z_vcpkg_property}${z_vcpkg_config}") - endforeach() - select_library_configurations(Z_VCPKG_DEFLATE) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${Z_VCPKG_DEFLATE_LIBRARIES}) - unset(z_vcpkg_config) - unset(z_vcpkg_libdeflate_configs) - unset(z_vcpkg_libdeflate_target) - unset(z_vcpkg_property) - unset(Z_VCPKG_DEFLATE_FOUND) - endif() - if("@zlib@") - find_package(ZLIB ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${ZLIB_LIBRARIES}) - endif() - if(UNIX) - list(APPEND z_vcpkg_tiff_link_libraries m) - list(APPEND z_vcpkg_tiff_libraries m) - endif() - - if(TARGET TIFF::TIFF) - set_property(TARGET TIFF::TIFF APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${z_vcpkg_tiff_link_libraries}) - endif() - list(APPEND TIFF_LIBRARIES ${z_vcpkg_tiff_libraries}) - unset(z_vcpkg_tiff_link_libraries) - unset(z_vcpkg_tiff_libraries) -endif() -unset(z_vcpkg_tiff_find_options) -cmake_policy(POP) diff --git a/dependencies/vcpkg_overlay_ports/tiff/vcpkg.json b/dependencies/vcpkg_overlay_ports/tiff/vcpkg.json deleted file mode 100644 index 9b36e1a8..00000000 --- a/dependencies/vcpkg_overlay_ports/tiff/vcpkg.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "tiff", - "version": "4.6.0", - "port-version": 2, - "description": "A library that supports the manipulation of TIFF image files", - "homepage": "https://libtiff.gitlab.io/libtiff/", - "license": "libtiff", - "dependencies": [ - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - "jpeg", - "zip" - ], - "features": { - "cxx": { - "description": "Build C++ libtiffxx library" - }, - "jpeg": { - "description": "Support JPEG compression in TIFF image files", - "dependencies": [ - "libjpeg-turbo" - ] - }, - "libdeflate": { - "description": "Use libdeflate for faster ZIP support", - "dependencies": [ - "libdeflate", - { - "name": "tiff", - "default-features": false, - "features": [ - "zip" - ] - } - ] - }, - "tools": { - "description": "Build tools" - }, - "webp": { - "description": "Support WEBP compression in TIFF image files", - "dependencies": [ - "libwebp" - ] - }, - "zip": { - "description": "Support ZIP/deflate compression in TIFF image files", - "dependencies": [ - "zlib" - ] - }, - "zstd": { - "description": "Support ZSTD compression in TIFF image files", - "dependencies": [ - "zstd" - ] - } - } -} diff --git a/dependencies/vcpkg_overlay_ports_linux/dbus/cmake.dep.patch b/dependencies/vcpkg_overlay_ports_linux/dbus/cmake.dep.patch deleted file mode 100644 index ac827f0c..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/dbus/cmake.dep.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt -index 8cde1ffe0..d4d09f223 100644 ---- a/tools/CMakeLists.txt -+++ b/tools/CMakeLists.txt -@@ -91,7 +91,9 @@ endif() - add_executable(dbus-launch ${dbus_launch_SOURCES}) - target_link_libraries(dbus-launch ${DBUS_LIBRARIES}) - if(DBUS_BUILD_X11) -- target_link_libraries(dbus-launch ${X11_LIBRARIES} ) -+ find_package(Threads REQUIRED) -+ target_link_libraries(dbus-launch ${X11_LIBRARIES} ${X11_xcb_LIB} ${X11_Xau_LIB} ${X11_Xdmcp_LIB} Threads::Threads) -+ target_include_directories(dbus-launch PRIVATE ${X11_INCLUDE_DIR}) - endif() - install(TARGETS dbus-launch ${INSTALL_TARGETS_DEFAULT_ARGS}) - diff --git a/dependencies/vcpkg_overlay_ports_linux/dbus/getpeereid.patch b/dependencies/vcpkg_overlay_ports_linux/dbus/getpeereid.patch deleted file mode 100644 index 5cd2309e..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/dbus/getpeereid.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/cmake/ConfigureChecks.cmake b/cmake/ConfigureChecks.cmake -index b7f3702..e2336ba 100644 ---- a/cmake/ConfigureChecks.cmake -+++ b/cmake/ConfigureChecks.cmake -@@ -51,6 +51,7 @@ check_symbol_exists(closefrom "unistd.h" HAVE_CLOSEFROM) # - check_symbol_exists(environ "unistd.h" HAVE_DECL_ENVIRON) - check_symbol_exists(fstatfs "sys/vfs.h" HAVE_FSTATFS) - check_symbol_exists(getgrouplist "grp.h" HAVE_GETGROUPLIST) # dbus-sysdeps.c -+check_symbol_exists(getpeereid "sys/types.h;unistd.h" HAVE_GETPEEREID) # dbus-sysdeps.c, - check_symbol_exists(getpeerucred "ucred.h" HAVE_GETPEERUCRED) # dbus-sysdeps.c, dbus-sysdeps-win.c - check_symbol_exists(getpwnam_r "errno.h;pwd.h" HAVE_GETPWNAM_R) # dbus-sysdeps-util-unix.c - check_symbol_exists(getrandom "sys/random.h" HAVE_GETRANDOM) -diff --git a/cmake/config.h.cmake b/cmake/config.h.cmake -index 77fc19c..2f25643 100644 ---- a/cmake/config.h.cmake -+++ b/cmake/config.h.cmake -@@ -140,6 +140,9 @@ - /* Define to 1 if you have getgrouplist */ - #cmakedefine HAVE_GETGROUPLIST 1 - -+/* Define to 1 if you have getpeereid */ -+#cmakedefine HAVE_GETPEEREID 1 -+ - /* Define to 1 if you have getpeerucred */ - #cmakedefine HAVE_GETPEERUCRED 1 - diff --git a/dependencies/vcpkg_overlay_ports_linux/dbus/libsystemd.patch b/dependencies/vcpkg_overlay_ports_linux/dbus/libsystemd.patch deleted file mode 100644 index 74193dc4..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/dbus/libsystemd.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index d3ec71b..932066a 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -141,6 +141,10 @@ if(DBUS_LINUX) - if(ENABLE_SYSTEMD AND SYSTEMD_FOUND) - set(DBUS_BUS_ENABLE_SYSTEMD ON) - set(HAVE_SYSTEMD ${SYSTEMD_FOUND}) -+ pkg_check_modules(SYSTEMD libsystemd IMPORTED_TARGET) -+ set(SYSTEMD_LIBRARIES PkgConfig::SYSTEMD CACHE INTERNAL "") -+ else() -+ set(SYSTEMD_LIBRARIES "" CACHE INTERNAL "") - endif() - option(ENABLE_USER_SESSION "enable user-session semantics for session bus under systemd" OFF) - set(DBUS_ENABLE_USER_SESSION ${ENABLE_USER_SESSION}) diff --git a/dependencies/vcpkg_overlay_ports_linux/dbus/pkgconfig.patch b/dependencies/vcpkg_overlay_ports_linux/dbus/pkgconfig.patch deleted file mode 100644 index 63581487..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/dbus/pkgconfig.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index caef738..b878f42 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -724,11 +724,11 @@ add_custom_target(help-options - # - if(DBUS_ENABLE_PKGCONFIG) - set(PLATFORM_LIBS pthread ${LIBRT}) -- if(PKG_CONFIG_FOUND) -- # convert lists of link libraries into -lstdc++ -lm etc.. -- foreach(LIB ${CMAKE_C_IMPLICIT_LINK_LIBRARIES} ${PLATFORM_LIBS}) -- set(LIBDBUS_LIBS "${LIBDBUS_LIBS} -l${LIB}") -- endforeach() -+ if(1) -+ set(LIBDBUS_LIBS "${CMAKE_THREAD_LIBS_INIT}") -+ if(LIBRT) -+ string(APPEND LIBDBUS_LIBS " -lrt") -+ endif() - set(original_prefix "${CMAKE_INSTALL_PREFIX}") - if(DBUS_RELOCATABLE) - set(pkgconfig_prefix "\${pcfiledir}/../..") diff --git a/dependencies/vcpkg_overlay_ports_linux/dbus/portfile.cmake b/dependencies/vcpkg_overlay_ports_linux/dbus/portfile.cmake deleted file mode 100644 index 56c7e182..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/dbus/portfile.cmake +++ /dev/null @@ -1,88 +0,0 @@ -vcpkg_check_linkage(ONLY_DYNAMIC_LIBRARY) - -vcpkg_from_gitlab( - GITLAB_URL https://gitlab.freedesktop.org/ - OUT_SOURCE_PATH SOURCE_PATH - REPO dbus/dbus - REF "dbus-${VERSION}" - SHA512 8e476b408514e6540c36beb84e8025827c22cda8958b6eb74d22b99c64765eb3cd5a6502aea546e3e5f0534039857b37edee89c659acef40e7cab0939947d4af - HEAD_REF master - PATCHES - cmake.dep.patch - pkgconfig.patch - getpeereid.patch # missing check from configure.ac - libsystemd.patch -) - -vcpkg_check_features(OUT_FEATURE_OPTIONS options - FEATURES - systemd ENABLE_SYSTEMD - x11 DBUS_BUILD_X11 - x11 CMAKE_REQUIRE_FIND_PACKAGE_X11 -) - -unset(ENV{DBUSDIR}) - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - OPTIONS - -DDBUS_BUILD_TESTS=OFF - -DDBUS_ENABLE_DOXYGEN_DOCS=OFF - -DDBUS_ENABLE_XML_DOCS=OFF - -DDBUS_INSTALL_SYSTEM_LIBS=OFF - #-DDBUS_SERVICE=ON - -DDBUS_WITH_GLIB=OFF - -DTHREADS_PREFER_PTHREAD_FLAG=ON - -DXSLTPROC_EXECUTABLE=FALSE - "-DCMAKE_INSTALL_SYSCONFDIR=${CURRENT_PACKAGES_DIR}/etc/${PORT}" - "-DWITH_SYSTEMD_SYSTEMUNITDIR=lib/systemd/system" - "-DWITH_SYSTEMD_USERUNITDIR=lib/systemd/user" - ${options} - OPTIONS_RELEASE - -DDBUS_DISABLE_ASSERT=OFF - -DDBUS_ENABLE_STATS=OFF - -DDBUS_ENABLE_VERBOSE_MODE=OFF - MAYBE_UNUSED_VARIABLES - DBUS_BUILD_X11 - DBUS_WITH_GLIB - ENABLE_SYSTEMD - THREADS_PREFER_PTHREAD_FLAG - WITH_SYSTEMD_SYSTEMUNITDIR - WITH_SYSTEMD_USERUNITDIR -) -vcpkg_cmake_install() -vcpkg_copy_pdbs() -vcpkg_cmake_config_fixup(PACKAGE_NAME "DBus1" CONFIG_PATH "lib/cmake/DBus1") -vcpkg_fixup_pkgconfig() - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" - "${CURRENT_PACKAGES_DIR}/debug/var/" - "${CURRENT_PACKAGES_DIR}/etc" - "${CURRENT_PACKAGES_DIR}/share/dbus-1/services" - "${CURRENT_PACKAGES_DIR}/share/dbus-1/session.d" - "${CURRENT_PACKAGES_DIR}/share/dbus-1/system-services" - "${CURRENT_PACKAGES_DIR}/share/dbus-1/system.d" - "${CURRENT_PACKAGES_DIR}/share/dbus-1/system.conf" - "${CURRENT_PACKAGES_DIR}/share/dbus-1/system.conf" - "${CURRENT_PACKAGES_DIR}/share/doc" - "${CURRENT_PACKAGES_DIR}/var" -) - -vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/share/dbus-1/session.conf" "${CURRENT_PACKAGES_DIR}/etc/dbus/dbus-1/session.conf" "") -vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/share/dbus-1/session.conf" "${CURRENT_PACKAGES_DIR}/etc/dbus/dbus-1/session.d" "") -vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/share/dbus-1/session.conf" "${CURRENT_PACKAGES_DIR}/etc/dbus/dbus-1/session-local.conf" "") - -set(TOOLS daemon launch monitor run-session send test-tool update-activation-environment) -if(VCPKG_TARGET_IS_WINDOWS) - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/${PORT}") - file(RENAME "${CURRENT_PACKAGES_DIR}/bin/dbus-env.bat" "${CURRENT_PACKAGES_DIR}/tools/${PORT}/dbus-env.bat") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/tools/${PORT}/dbus-env.bat" "${CURRENT_PACKAGES_DIR}" "%~dp0/../..") -else() - list(APPEND TOOLS cleanup-sockets uuidgen) -endif() -list(TRANSFORM TOOLS PREPEND "dbus-" ) -vcpkg_copy_tools(TOOL_NAMES ${TOOLS} AUTO_CLEAN) - -file(INSTALL "${SOURCE_PATH}/COPYING" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright) diff --git a/dependencies/vcpkg_overlay_ports_linux/dbus/vcpkg.json b/dependencies/vcpkg_overlay_ports_linux/dbus/vcpkg.json deleted file mode 100644 index 853dff05..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/dbus/vcpkg.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "dbus", - "version": "1.15.8", - "port-version": 2, - "description": "D-Bus specification and reference implementation, including libdbus and dbus-daemon", - "homepage": "https://gitlab.freedesktop.org/dbus/dbus", - "license": "AFL-2.1 OR GPL-2.0-or-later", - "supports": "!uwp & !staticcrt", - "dependencies": [ - "expat", - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - ], - "features": { - "x11": { - "description": "Build with X11 autolaunch support", - "dependencies": [ - "libx11" - ] - } - } -} diff --git a/dependencies/vcpkg_overlay_ports_linux/sdl2/alsa-dep-fix.patch b/dependencies/vcpkg_overlay_ports_linux/sdl2/alsa-dep-fix.patch deleted file mode 100644 index 5b2c77b9..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/sdl2/alsa-dep-fix.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/SDL2Config.cmake.in b/SDL2Config.cmake.in -index cc8bcf26d..ead829767 100644 ---- a/SDL2Config.cmake.in -+++ b/SDL2Config.cmake.in -@@ -35,7 +35,7 @@ include("${CMAKE_CURRENT_LIST_DIR}/sdlfind.cmake") - - set(SDL_ALSA @SDL_ALSA@) - set(SDL_ALSA_SHARED @SDL_ALSA_SHARED@) --if(SDL_ALSA AND NOT SDL_ALSA_SHARED AND TARGET SDL2::SDL2-static) -+if(SDL_ALSA) - sdlFindALSA() - endif() - unset(SDL_ALSA) diff --git a/dependencies/vcpkg_overlay_ports_linux/sdl2/deps.patch b/dependencies/vcpkg_overlay_ports_linux/sdl2/deps.patch deleted file mode 100644 index a8637d8c..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/sdl2/deps.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake -index 65a98efbe..2f99f28f1 100644 ---- a/cmake/sdlchecks.cmake -+++ b/cmake/sdlchecks.cmake -@@ -352,7 +352,7 @@ endmacro() - # - HAVE_SDL_LOADSO opt - macro(CheckLibSampleRate) - if(SDL_LIBSAMPLERATE) -- find_package(SampleRate QUIET) -+ find_package(SampleRate CONFIG REQUIRED) - if(SampleRate_FOUND AND TARGET SampleRate::samplerate) - set(HAVE_LIBSAMPLERATE TRUE) - set(HAVE_LIBSAMPLERATE_H TRUE) diff --git a/dependencies/vcpkg_overlay_ports_linux/sdl2/portfile.cmake b/dependencies/vcpkg_overlay_ports_linux/sdl2/portfile.cmake deleted file mode 100644 index 22685e6a..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/sdl2/portfile.cmake +++ /dev/null @@ -1,137 +0,0 @@ -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO libsdl-org/SDL - REF "release-${VERSION}" - SHA512 c7635a83a52f3970a372b804a8631f0a7e6b8d89aed1117bcc54a2040ad0928122175004cf2b42cf84a4fd0f86236f779229eaa63dfa6ca9c89517f999c5ff1c - HEAD_REF main - PATCHES - deps.patch - alsa-dep-fix.patch -) - -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "static" SDL_STATIC) -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" SDL_SHARED) -string(COMPARE EQUAL "${VCPKG_CRT_LINKAGE}" "static" FORCE_STATIC_VCRT) - -vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - alsa SDL_ALSA - alsa CMAKE_REQUIRE_FIND_PACKAGE_ALSA - ibus SDL_IBUS - samplerate SDL_LIBSAMPLERATE - vulkan SDL_VULKAN - wayland SDL_WAYLAND - x11 SDL_X11 - INVERTED_FEATURES - alsa CMAKE_DISABLE_FIND_PACKAGE_ALSA -) - -if ("x11" IN_LIST FEATURES) - message(WARNING "You will need to install Xorg dependencies to use feature x11:\nsudo apt install libx11-dev libxft-dev libxext-dev\n") -endif() -if ("wayland" IN_LIST FEATURES) - message(WARNING "You will need to install Wayland dependencies to use feature wayland:\nsudo apt install libwayland-dev libxkbcommon-dev libegl1-mesa-dev\n") -endif() -if ("ibus" IN_LIST FEATURES) - message(WARNING "You will need to install ibus dependencies to use feature ibus:\nsudo apt install libibus-1.0-dev\n") -endif() - -if(VCPKG_TARGET_IS_UWP) - set(configure_opts WINDOWS_USE_MSBUILD) -endif() - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - ${configure_opts} - OPTIONS ${FEATURE_OPTIONS} - -DSDL_STATIC=${SDL_STATIC} - -DSDL_SHARED=${SDL_SHARED} - -DSDL_FORCE_STATIC_VCRT=${FORCE_STATIC_VCRT} - -DSDL_LIBC=ON - -DSDL_TEST=OFF - -DSDL_INSTALL_CMAKEDIR="cmake" - -DCMAKE_DISABLE_FIND_PACKAGE_Git=ON - -DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=ON - -DSDL_LIBSAMPLERATE_SHARED=OFF - MAYBE_UNUSED_VARIABLES - SDL_FORCE_STATIC_VCRT - PKG_CONFIG_USE_CMAKE_PREFIX_PATH -) - -vcpkg_cmake_install() -vcpkg_cmake_config_fixup(CONFIG_PATH cmake) - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" - "${CURRENT_PACKAGES_DIR}/bin/sdl2-config" - "${CURRENT_PACKAGES_DIR}/debug/bin/sdl2-config" - "${CURRENT_PACKAGES_DIR}/SDL2.framework" - "${CURRENT_PACKAGES_DIR}/debug/SDL2.framework" - "${CURRENT_PACKAGES_DIR}/share/licenses" - "${CURRENT_PACKAGES_DIR}/share/aclocal" -) - -file(GLOB BINS "${CURRENT_PACKAGES_DIR}/debug/bin/*" "${CURRENT_PACKAGES_DIR}/bin/*") -if(NOT BINS) - file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/bin" - "${CURRENT_PACKAGES_DIR}/debug/bin" - ) -endif() - -if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_UWP AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/lib/manual-link") - file(RENAME "${CURRENT_PACKAGES_DIR}/lib/SDL2main.lib" "${CURRENT_PACKAGES_DIR}/lib/manual-link/SDL2main.lib") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link") - file(RENAME "${CURRENT_PACKAGES_DIR}/debug/lib/SDL2maind.lib" "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link/SDL2maind.lib") - endif() - - file(GLOB SHARE_FILES "${CURRENT_PACKAGES_DIR}/share/sdl2/*.cmake") - foreach(SHARE_FILE ${SHARE_FILES}) - vcpkg_replace_string("${SHARE_FILE}" "lib/SDL2main" "lib/manual-link/SDL2main") - endforeach() -endif() - -vcpkg_copy_pdbs() - -set(DYLIB_COMPATIBILITY_VERSION_REGEX "set\\(DYLIB_COMPATIBILITY_VERSION (.+)\\)") -set(DYLIB_CURRENT_VERSION_REGEX "set\\(DYLIB_CURRENT_VERSION (.+)\\)") -file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_COMPATIBILITY_VERSION REGEX ${DYLIB_COMPATIBILITY_VERSION_REGEX}) -file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_CURRENT_VERSION REGEX ${DYLIB_CURRENT_VERSION_REGEX}) -string(REGEX REPLACE ${DYLIB_COMPATIBILITY_VERSION_REGEX} "\\1" DYLIB_COMPATIBILITY_VERSION "${DYLIB_COMPATIBILITY_VERSION}") -string(REGEX REPLACE ${DYLIB_CURRENT_VERSION_REGEX} "\\1" DYLIB_CURRENT_VERSION "${DYLIB_CURRENT_VERSION}") - -if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2main" "-lSDL2maind") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2 " "-lSDL2d ") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-static " "-lSDL2-staticd ") -endif() - -if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic" AND VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-lSDL2-static " " ") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-staticd " " ") - endif() -endif() - -if(VCPKG_TARGET_IS_UWP) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "d") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") - endif() -endif() - -vcpkg_fixup_pkgconfig() - -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") diff --git a/dependencies/vcpkg_overlay_ports_linux/sdl2/usage b/dependencies/vcpkg_overlay_ports_linux/sdl2/usage deleted file mode 100644 index 1cddcd46..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/sdl2/usage +++ /dev/null @@ -1,8 +0,0 @@ -sdl2 provides CMake targets: - - find_package(SDL2 CONFIG REQUIRED) - target_link_libraries(main - PRIVATE - $ - $,SDL2::SDL2,SDL2::SDL2-static> - ) diff --git a/dependencies/vcpkg_overlay_ports_linux/sdl2/vcpkg.json b/dependencies/vcpkg_overlay_ports_linux/sdl2/vcpkg.json deleted file mode 100644 index 1f460375..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/sdl2/vcpkg.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "sdl2", - "version": "2.30.0", - "description": "Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.", - "homepage": "https://www.libsdl.org/download-2.0.php", - "license": "Zlib", - "dependencies": [ - { - "name": "dbus", - "default-features": false, - "platform": "linux" - }, - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - { - "name": "ibus", - "platform": "linux" - }, - { - "name": "wayland", - "platform": "linux" - }, - { - "name": "x11", - "platform": "linux" - } - ], - "features": { - "alsa": { - "description": "Support for alsa audio", - "dependencies": [ - { - "name": "alsa", - "platform": "linux" - } - ] - }, - "ibus": { - "description": "Build with ibus IME support", - "supports": "linux" - }, - "samplerate": { - "description": "Use libsamplerate for audio rate conversion", - "dependencies": [ - "libsamplerate" - ] - }, - "vulkan": { - "description": "Vulkan functionality for SDL" - }, - "wayland": { - "description": "Build with Wayland support", - "supports": "linux" - }, - "x11": { - "description": "Build with X11 support", - "supports": "!windows" - } - } -} diff --git a/dependencies/vcpkg_overlay_ports_linux/tiff/FindCMath.patch b/dependencies/vcpkg_overlay_ports_linux/tiff/FindCMath.patch deleted file mode 100644 index 70654cf8..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/tiff/FindCMath.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/FindCMath.cmake b/cmake/FindCMath.cmake -index ad92218..dd42aba 100644 ---- a/cmake/FindCMath.cmake -+++ b/cmake/FindCMath.cmake -@@ -31,7 +31,7 @@ include(CheckSymbolExists) - include(CheckLibraryExists) - - check_symbol_exists(pow "math.h" CMath_HAVE_LIBC_POW) --find_library(CMath_LIBRARY NAMES m) -+find_library(CMath_LIBRARY NAMES m PATHS ${CMAKE_C_IMPLICIT_LINK_DIRECTORIES}) - - if(NOT CMath_HAVE_LIBC_POW) - set(CMAKE_REQUIRED_LIBRARIES_SAVE ${CMAKE_REQUIRED_LIBRARIES}) diff --git a/dependencies/vcpkg_overlay_ports_linux/tiff/portfile.cmake b/dependencies/vcpkg_overlay_ports_linux/tiff/portfile.cmake deleted file mode 100644 index 426d8af7..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/tiff/portfile.cmake +++ /dev/null @@ -1,86 +0,0 @@ -vcpkg_from_gitlab( - GITLAB_URL https://gitlab.com - OUT_SOURCE_PATH SOURCE_PATH - REPO libtiff/libtiff - REF "v${VERSION}" - SHA512 ef2f1d424219d9e245069b7d23e78f5e817cf6ee516d46694915ab6c8909522166f84997513d20a702f4e52c3f18467813935b328fafa34bea5156dee00f66fa - HEAD_REF master - PATCHES - FindCMath.patch -) - -vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - cxx cxx - jpeg jpeg - jpeg CMAKE_REQUIRE_FIND_PACKAGE_JPEG - libdeflate libdeflate - libdeflate CMAKE_REQUIRE_FIND_PACKAGE_Deflate - lzma lzma - lzma CMAKE_REQUIRE_FIND_PACKAGE_liblzma - tools tiff-tools - webp webp - webp CMAKE_REQUIRE_FIND_PACKAGE_WebP - zip zlib - zip CMAKE_REQUIRE_FIND_PACKAGE_ZLIB - zstd zstd - zstd CMAKE_REQUIRE_FIND_PACKAGE_ZSTD -) - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - OPTIONS - ${FEATURE_OPTIONS} - -DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON - -Dtiff-docs=OFF - -Dtiff-contrib=OFF - -Dtiff-tests=OFF - -Djbig=OFF # This is disabled by default due to GPL/Proprietary licensing. - -Djpeg12=OFF - -Dlerc=OFF - -DCMAKE_DISABLE_FIND_PACKAGE_OpenGL=ON - -DCMAKE_DISABLE_FIND_PACKAGE_GLUT=ON - -DZSTD_HAVE_DECOMPRESS_STREAM=ON - -DHAVE_JPEGTURBO_DUAL_MODE_8_12=OFF - OPTIONS_DEBUG - -DCMAKE_DEBUG_POSTFIX=d # tiff sets "d" for MSVC only. - MAYBE_UNUSED_VARIABLES - CMAKE_DISABLE_FIND_PACKAGE_GLUT - CMAKE_DISABLE_FIND_PACKAGE_OpenGL - ZSTD_HAVE_DECOMPRESS_STREAM -) - -vcpkg_cmake_install() - -# CMake config wasn't packaged in the past and is not yet usable now, -# cf. https://gitlab.com/libtiff/libtiff/-/merge_requests/496 -# vcpkg_cmake_config_fixup(CONFIG_PATH "lib/cmake/tiff") -file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/lib/cmake" "${CURRENT_PACKAGES_DIR}/debug/lib/cmake") - -set(_file "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libtiff-4.pc") -if(EXISTS "${_file}") - vcpkg_replace_string("${_file}" "-ltiff" "-ltiffd") -endif() -vcpkg_fixup_pkgconfig() - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" -) - -configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake.in" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-cmake-wrapper.cmake" @ONLY) - -if ("tools" IN_LIST FEATURES) - vcpkg_copy_tools(TOOL_NAMES - tiffcp - tiffdump - tiffinfo - tiffset - tiffsplit - AUTO_CLEAN - ) -endif() - -vcpkg_copy_pdbs() -file(COPY "${CURRENT_PORT_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") diff --git a/dependencies/vcpkg_overlay_ports_linux/tiff/usage b/dependencies/vcpkg_overlay_ports_linux/tiff/usage deleted file mode 100644 index d47265b1..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/tiff/usage +++ /dev/null @@ -1,9 +0,0 @@ -tiff is compatible with built-in CMake targets: - - find_package(TIFF REQUIRED) - target_link_libraries(main PRIVATE TIFF::TIFF) - -tiff provides pkg-config modules: - - # Tag Image File Format (TIFF) library. - libtiff-4 diff --git a/dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg-cmake-wrapper.cmake.in b/dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg-cmake-wrapper.cmake.in deleted file mode 100644 index 1d04ec7a..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg-cmake-wrapper.cmake.in +++ /dev/null @@ -1,104 +0,0 @@ -cmake_policy(PUSH) -cmake_policy(SET CMP0012 NEW) -cmake_policy(SET CMP0057 NEW) -set(z_vcpkg_tiff_find_options "") -if("REQUIRED" IN_LIST ARGS) - list(APPEND z_vcpkg_tiff_find_options "REQUIRED") -endif() -if("QUIET" IN_LIST ARGS) - list(APPEND z_vcpkg_tiff_find_options "QUIET") -endif() - -_find_package(${ARGS}) - -if(TIFF_FOUND AND "@VCPKG_LIBRARY_LINKAGE@" STREQUAL "static") - include(SelectLibraryConfigurations) - set(z_vcpkg_tiff_link_libraries "") - set(z_vcpkg_tiff_libraries "") - if("@webp@") - find_package(WebP CONFIG ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${WebP_LIBRARIES}) - endif() - if("@lzma@") - find_package(LibLZMA ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${LIBLZMA_LIBRARIES}) - endif() - if("@jpeg@") - find_package(JPEG ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${JPEG_LIBRARIES}) - endif() - if("@zstd@") - find_package(zstd CONFIG ${z_vcpkg_tiff_find_options}) - set(z_vcpkg_tiff_zstd_target_property "IMPORTED_LOCATION_") - if(TARGET zstd::libzstd_shared) - set(z_vcpkg_tiff_zstd "\$") - set(z_vcpkg_tiff_zstd_target zstd::libzstd_shared) - if(WIN32) - set(z_vcpkg_tiff_zstd_target_property "IMPORTED_IMPLIB_") - endif() - else() - set(z_vcpkg_tiff_zstd "\$") - set(z_vcpkg_tiff_zstd_target zstd::libzstd_static) - endif() - get_target_property(z_vcpkg_tiff_zstd_configs "${z_vcpkg_tiff_zstd_target}" IMPORTED_CONFIGURATIONS) - foreach(z_vcpkg_config IN LISTS z_vcpkg_tiff_zstd_configs) - get_target_property(ZSTD_LIBRARY_${z_vcpkg_config} "${z_vcpkg_tiff_zstd_target}" "${z_vcpkg_tiff_zstd_target_property}${z_vcpkg_config}") - endforeach() - select_library_configurations(ZSTD) - if(NOT TARGET ZSTD::ZSTD) - add_library(ZSTD::ZSTD INTERFACE IMPORTED) - set_property(TARGET ZSTD::ZSTD APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${z_vcpkg_tiff_zstd}) - endif() - list(APPEND z_vcpkg_tiff_link_libraries ${z_vcpkg_tiff_zstd}) - list(APPEND z_vcpkg_tiff_libraries ${ZSTD_LIBRARIES}) - unset(z_vcpkg_tiff_zstd) - unset(z_vcpkg_tiff_zstd_configs) - unset(z_vcpkg_config) - unset(z_vcpkg_tiff_zstd_target) - endif() - if("@libdeflate@") - find_package(libdeflate ${z_vcpkg_tiff_find_options}) - set(z_vcpkg_property "IMPORTED_LOCATION_") - if(TARGET libdeflate::libdeflate_shared) - set(z_vcpkg_libdeflate_target libdeflate::libdeflate_shared) - if(WIN32) - set(z_vcpkg_property "IMPORTED_IMPLIB_") - endif() - else() - set(z_vcpkg_libdeflate_target libdeflate::libdeflate_static) - endif() - get_target_property(z_vcpkg_libdeflate_configs "${z_vcpkg_libdeflate_target}" IMPORTED_CONFIGURATIONS) - foreach(z_vcpkg_config IN LISTS z_vcpkg_libdeflate_configs) - get_target_property(Z_VCPKG_DEFLATE_LIBRARY_${z_vcpkg_config} "${z_vcpkg_libdeflate_target}" "${z_vcpkg_property}${z_vcpkg_config}") - endforeach() - select_library_configurations(Z_VCPKG_DEFLATE) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${Z_VCPKG_DEFLATE_LIBRARIES}) - unset(z_vcpkg_config) - unset(z_vcpkg_libdeflate_configs) - unset(z_vcpkg_libdeflate_target) - unset(z_vcpkg_property) - unset(Z_VCPKG_DEFLATE_FOUND) - endif() - if("@zlib@") - find_package(ZLIB ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${ZLIB_LIBRARIES}) - endif() - if(UNIX) - list(APPEND z_vcpkg_tiff_link_libraries m) - list(APPEND z_vcpkg_tiff_libraries m) - endif() - - if(TARGET TIFF::TIFF) - set_property(TARGET TIFF::TIFF APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${z_vcpkg_tiff_link_libraries}) - endif() - list(APPEND TIFF_LIBRARIES ${z_vcpkg_tiff_libraries}) - unset(z_vcpkg_tiff_link_libraries) - unset(z_vcpkg_tiff_libraries) -endif() -unset(z_vcpkg_tiff_find_options) -cmake_policy(POP) diff --git a/dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg.json b/dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg.json deleted file mode 100644 index 9b36e1a8..00000000 --- a/dependencies/vcpkg_overlay_ports_linux/tiff/vcpkg.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "tiff", - "version": "4.6.0", - "port-version": 2, - "description": "A library that supports the manipulation of TIFF image files", - "homepage": "https://libtiff.gitlab.io/libtiff/", - "license": "libtiff", - "dependencies": [ - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - "jpeg", - "zip" - ], - "features": { - "cxx": { - "description": "Build C++ libtiffxx library" - }, - "jpeg": { - "description": "Support JPEG compression in TIFF image files", - "dependencies": [ - "libjpeg-turbo" - ] - }, - "libdeflate": { - "description": "Use libdeflate for faster ZIP support", - "dependencies": [ - "libdeflate", - { - "name": "tiff", - "default-features": false, - "features": [ - "zip" - ] - } - ] - }, - "tools": { - "description": "Build tools" - }, - "webp": { - "description": "Support WEBP compression in TIFF image files", - "dependencies": [ - "libwebp" - ] - }, - "zip": { - "description": "Support ZIP/deflate compression in TIFF image files", - "dependencies": [ - "zlib" - ] - }, - "zstd": { - "description": "Support ZSTD compression in TIFF image files", - "dependencies": [ - "zstd" - ] - } - } -} diff --git a/dependencies/vcpkg_overlay_ports_mac/sdl2/alsa-dep-fix.patch b/dependencies/vcpkg_overlay_ports_mac/sdl2/alsa-dep-fix.patch deleted file mode 100644 index 5b2c77b9..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/sdl2/alsa-dep-fix.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/SDL2Config.cmake.in b/SDL2Config.cmake.in -index cc8bcf26d..ead829767 100644 ---- a/SDL2Config.cmake.in -+++ b/SDL2Config.cmake.in -@@ -35,7 +35,7 @@ include("${CMAKE_CURRENT_LIST_DIR}/sdlfind.cmake") - - set(SDL_ALSA @SDL_ALSA@) - set(SDL_ALSA_SHARED @SDL_ALSA_SHARED@) --if(SDL_ALSA AND NOT SDL_ALSA_SHARED AND TARGET SDL2::SDL2-static) -+if(SDL_ALSA) - sdlFindALSA() - endif() - unset(SDL_ALSA) diff --git a/dependencies/vcpkg_overlay_ports_mac/sdl2/deps.patch b/dependencies/vcpkg_overlay_ports_mac/sdl2/deps.patch deleted file mode 100644 index a8637d8c..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/sdl2/deps.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake -index 65a98efbe..2f99f28f1 100644 ---- a/cmake/sdlchecks.cmake -+++ b/cmake/sdlchecks.cmake -@@ -352,7 +352,7 @@ endmacro() - # - HAVE_SDL_LOADSO opt - macro(CheckLibSampleRate) - if(SDL_LIBSAMPLERATE) -- find_package(SampleRate QUIET) -+ find_package(SampleRate CONFIG REQUIRED) - if(SampleRate_FOUND AND TARGET SampleRate::samplerate) - set(HAVE_LIBSAMPLERATE TRUE) - set(HAVE_LIBSAMPLERATE_H TRUE) diff --git a/dependencies/vcpkg_overlay_ports_mac/sdl2/portfile.cmake b/dependencies/vcpkg_overlay_ports_mac/sdl2/portfile.cmake deleted file mode 100644 index 22685e6a..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/sdl2/portfile.cmake +++ /dev/null @@ -1,137 +0,0 @@ -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO libsdl-org/SDL - REF "release-${VERSION}" - SHA512 c7635a83a52f3970a372b804a8631f0a7e6b8d89aed1117bcc54a2040ad0928122175004cf2b42cf84a4fd0f86236f779229eaa63dfa6ca9c89517f999c5ff1c - HEAD_REF main - PATCHES - deps.patch - alsa-dep-fix.patch -) - -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "static" SDL_STATIC) -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" SDL_SHARED) -string(COMPARE EQUAL "${VCPKG_CRT_LINKAGE}" "static" FORCE_STATIC_VCRT) - -vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - alsa SDL_ALSA - alsa CMAKE_REQUIRE_FIND_PACKAGE_ALSA - ibus SDL_IBUS - samplerate SDL_LIBSAMPLERATE - vulkan SDL_VULKAN - wayland SDL_WAYLAND - x11 SDL_X11 - INVERTED_FEATURES - alsa CMAKE_DISABLE_FIND_PACKAGE_ALSA -) - -if ("x11" IN_LIST FEATURES) - message(WARNING "You will need to install Xorg dependencies to use feature x11:\nsudo apt install libx11-dev libxft-dev libxext-dev\n") -endif() -if ("wayland" IN_LIST FEATURES) - message(WARNING "You will need to install Wayland dependencies to use feature wayland:\nsudo apt install libwayland-dev libxkbcommon-dev libegl1-mesa-dev\n") -endif() -if ("ibus" IN_LIST FEATURES) - message(WARNING "You will need to install ibus dependencies to use feature ibus:\nsudo apt install libibus-1.0-dev\n") -endif() - -if(VCPKG_TARGET_IS_UWP) - set(configure_opts WINDOWS_USE_MSBUILD) -endif() - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - ${configure_opts} - OPTIONS ${FEATURE_OPTIONS} - -DSDL_STATIC=${SDL_STATIC} - -DSDL_SHARED=${SDL_SHARED} - -DSDL_FORCE_STATIC_VCRT=${FORCE_STATIC_VCRT} - -DSDL_LIBC=ON - -DSDL_TEST=OFF - -DSDL_INSTALL_CMAKEDIR="cmake" - -DCMAKE_DISABLE_FIND_PACKAGE_Git=ON - -DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=ON - -DSDL_LIBSAMPLERATE_SHARED=OFF - MAYBE_UNUSED_VARIABLES - SDL_FORCE_STATIC_VCRT - PKG_CONFIG_USE_CMAKE_PREFIX_PATH -) - -vcpkg_cmake_install() -vcpkg_cmake_config_fixup(CONFIG_PATH cmake) - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" - "${CURRENT_PACKAGES_DIR}/bin/sdl2-config" - "${CURRENT_PACKAGES_DIR}/debug/bin/sdl2-config" - "${CURRENT_PACKAGES_DIR}/SDL2.framework" - "${CURRENT_PACKAGES_DIR}/debug/SDL2.framework" - "${CURRENT_PACKAGES_DIR}/share/licenses" - "${CURRENT_PACKAGES_DIR}/share/aclocal" -) - -file(GLOB BINS "${CURRENT_PACKAGES_DIR}/debug/bin/*" "${CURRENT_PACKAGES_DIR}/bin/*") -if(NOT BINS) - file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/bin" - "${CURRENT_PACKAGES_DIR}/debug/bin" - ) -endif() - -if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_UWP AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/lib/manual-link") - file(RENAME "${CURRENT_PACKAGES_DIR}/lib/SDL2main.lib" "${CURRENT_PACKAGES_DIR}/lib/manual-link/SDL2main.lib") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link") - file(RENAME "${CURRENT_PACKAGES_DIR}/debug/lib/SDL2maind.lib" "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link/SDL2maind.lib") - endif() - - file(GLOB SHARE_FILES "${CURRENT_PACKAGES_DIR}/share/sdl2/*.cmake") - foreach(SHARE_FILE ${SHARE_FILES}) - vcpkg_replace_string("${SHARE_FILE}" "lib/SDL2main" "lib/manual-link/SDL2main") - endforeach() -endif() - -vcpkg_copy_pdbs() - -set(DYLIB_COMPATIBILITY_VERSION_REGEX "set\\(DYLIB_COMPATIBILITY_VERSION (.+)\\)") -set(DYLIB_CURRENT_VERSION_REGEX "set\\(DYLIB_CURRENT_VERSION (.+)\\)") -file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_COMPATIBILITY_VERSION REGEX ${DYLIB_COMPATIBILITY_VERSION_REGEX}) -file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_CURRENT_VERSION REGEX ${DYLIB_CURRENT_VERSION_REGEX}) -string(REGEX REPLACE ${DYLIB_COMPATIBILITY_VERSION_REGEX} "\\1" DYLIB_COMPATIBILITY_VERSION "${DYLIB_COMPATIBILITY_VERSION}") -string(REGEX REPLACE ${DYLIB_CURRENT_VERSION_REGEX} "\\1" DYLIB_CURRENT_VERSION "${DYLIB_CURRENT_VERSION}") - -if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2main" "-lSDL2maind") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2 " "-lSDL2d ") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-static " "-lSDL2-staticd ") -endif() - -if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic" AND VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-lSDL2-static " " ") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-staticd " " ") - endif() -endif() - -if(VCPKG_TARGET_IS_UWP) - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "d") - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") - endif() -endif() - -vcpkg_fixup_pkgconfig() - -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") diff --git a/dependencies/vcpkg_overlay_ports_mac/sdl2/usage b/dependencies/vcpkg_overlay_ports_mac/sdl2/usage deleted file mode 100644 index 1cddcd46..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/sdl2/usage +++ /dev/null @@ -1,8 +0,0 @@ -sdl2 provides CMake targets: - - find_package(SDL2 CONFIG REQUIRED) - target_link_libraries(main - PRIVATE - $ - $,SDL2::SDL2,SDL2::SDL2-static> - ) diff --git a/dependencies/vcpkg_overlay_ports_mac/sdl2/vcpkg.json b/dependencies/vcpkg_overlay_ports_mac/sdl2/vcpkg.json deleted file mode 100644 index 1f460375..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/sdl2/vcpkg.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "sdl2", - "version": "2.30.0", - "description": "Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.", - "homepage": "https://www.libsdl.org/download-2.0.php", - "license": "Zlib", - "dependencies": [ - { - "name": "dbus", - "default-features": false, - "platform": "linux" - }, - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - { - "name": "ibus", - "platform": "linux" - }, - { - "name": "wayland", - "platform": "linux" - }, - { - "name": "x11", - "platform": "linux" - } - ], - "features": { - "alsa": { - "description": "Support for alsa audio", - "dependencies": [ - { - "name": "alsa", - "platform": "linux" - } - ] - }, - "ibus": { - "description": "Build with ibus IME support", - "supports": "linux" - }, - "samplerate": { - "description": "Use libsamplerate for audio rate conversion", - "dependencies": [ - "libsamplerate" - ] - }, - "vulkan": { - "description": "Vulkan functionality for SDL" - }, - "wayland": { - "description": "Build with Wayland support", - "supports": "linux" - }, - "x11": { - "description": "Build with X11 support", - "supports": "!windows" - } - } -} diff --git a/dependencies/vcpkg_overlay_ports_mac/tiff/FindCMath.patch b/dependencies/vcpkg_overlay_ports_mac/tiff/FindCMath.patch deleted file mode 100644 index 70654cf8..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/tiff/FindCMath.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/FindCMath.cmake b/cmake/FindCMath.cmake -index ad92218..dd42aba 100644 ---- a/cmake/FindCMath.cmake -+++ b/cmake/FindCMath.cmake -@@ -31,7 +31,7 @@ include(CheckSymbolExists) - include(CheckLibraryExists) - - check_symbol_exists(pow "math.h" CMath_HAVE_LIBC_POW) --find_library(CMath_LIBRARY NAMES m) -+find_library(CMath_LIBRARY NAMES m PATHS ${CMAKE_C_IMPLICIT_LINK_DIRECTORIES}) - - if(NOT CMath_HAVE_LIBC_POW) - set(CMAKE_REQUIRED_LIBRARIES_SAVE ${CMAKE_REQUIRED_LIBRARIES}) diff --git a/dependencies/vcpkg_overlay_ports_mac/tiff/portfile.cmake b/dependencies/vcpkg_overlay_ports_mac/tiff/portfile.cmake deleted file mode 100644 index 426d8af7..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/tiff/portfile.cmake +++ /dev/null @@ -1,86 +0,0 @@ -vcpkg_from_gitlab( - GITLAB_URL https://gitlab.com - OUT_SOURCE_PATH SOURCE_PATH - REPO libtiff/libtiff - REF "v${VERSION}" - SHA512 ef2f1d424219d9e245069b7d23e78f5e817cf6ee516d46694915ab6c8909522166f84997513d20a702f4e52c3f18467813935b328fafa34bea5156dee00f66fa - HEAD_REF master - PATCHES - FindCMath.patch -) - -vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - cxx cxx - jpeg jpeg - jpeg CMAKE_REQUIRE_FIND_PACKAGE_JPEG - libdeflate libdeflate - libdeflate CMAKE_REQUIRE_FIND_PACKAGE_Deflate - lzma lzma - lzma CMAKE_REQUIRE_FIND_PACKAGE_liblzma - tools tiff-tools - webp webp - webp CMAKE_REQUIRE_FIND_PACKAGE_WebP - zip zlib - zip CMAKE_REQUIRE_FIND_PACKAGE_ZLIB - zstd zstd - zstd CMAKE_REQUIRE_FIND_PACKAGE_ZSTD -) - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - OPTIONS - ${FEATURE_OPTIONS} - -DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON - -Dtiff-docs=OFF - -Dtiff-contrib=OFF - -Dtiff-tests=OFF - -Djbig=OFF # This is disabled by default due to GPL/Proprietary licensing. - -Djpeg12=OFF - -Dlerc=OFF - -DCMAKE_DISABLE_FIND_PACKAGE_OpenGL=ON - -DCMAKE_DISABLE_FIND_PACKAGE_GLUT=ON - -DZSTD_HAVE_DECOMPRESS_STREAM=ON - -DHAVE_JPEGTURBO_DUAL_MODE_8_12=OFF - OPTIONS_DEBUG - -DCMAKE_DEBUG_POSTFIX=d # tiff sets "d" for MSVC only. - MAYBE_UNUSED_VARIABLES - CMAKE_DISABLE_FIND_PACKAGE_GLUT - CMAKE_DISABLE_FIND_PACKAGE_OpenGL - ZSTD_HAVE_DECOMPRESS_STREAM -) - -vcpkg_cmake_install() - -# CMake config wasn't packaged in the past and is not yet usable now, -# cf. https://gitlab.com/libtiff/libtiff/-/merge_requests/496 -# vcpkg_cmake_config_fixup(CONFIG_PATH "lib/cmake/tiff") -file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/lib/cmake" "${CURRENT_PACKAGES_DIR}/debug/lib/cmake") - -set(_file "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libtiff-4.pc") -if(EXISTS "${_file}") - vcpkg_replace_string("${_file}" "-ltiff" "-ltiffd") -endif() -vcpkg_fixup_pkgconfig() - -file(REMOVE_RECURSE - "${CURRENT_PACKAGES_DIR}/debug/include" - "${CURRENT_PACKAGES_DIR}/debug/share" -) - -configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake.in" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-cmake-wrapper.cmake" @ONLY) - -if ("tools" IN_LIST FEATURES) - vcpkg_copy_tools(TOOL_NAMES - tiffcp - tiffdump - tiffinfo - tiffset - tiffsplit - AUTO_CLEAN - ) -endif() - -vcpkg_copy_pdbs() -file(COPY "${CURRENT_PORT_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") diff --git a/dependencies/vcpkg_overlay_ports_mac/tiff/usage b/dependencies/vcpkg_overlay_ports_mac/tiff/usage deleted file mode 100644 index d47265b1..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/tiff/usage +++ /dev/null @@ -1,9 +0,0 @@ -tiff is compatible with built-in CMake targets: - - find_package(TIFF REQUIRED) - target_link_libraries(main PRIVATE TIFF::TIFF) - -tiff provides pkg-config modules: - - # Tag Image File Format (TIFF) library. - libtiff-4 diff --git a/dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg-cmake-wrapper.cmake.in b/dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg-cmake-wrapper.cmake.in deleted file mode 100644 index 1d04ec7a..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg-cmake-wrapper.cmake.in +++ /dev/null @@ -1,104 +0,0 @@ -cmake_policy(PUSH) -cmake_policy(SET CMP0012 NEW) -cmake_policy(SET CMP0057 NEW) -set(z_vcpkg_tiff_find_options "") -if("REQUIRED" IN_LIST ARGS) - list(APPEND z_vcpkg_tiff_find_options "REQUIRED") -endif() -if("QUIET" IN_LIST ARGS) - list(APPEND z_vcpkg_tiff_find_options "QUIET") -endif() - -_find_package(${ARGS}) - -if(TIFF_FOUND AND "@VCPKG_LIBRARY_LINKAGE@" STREQUAL "static") - include(SelectLibraryConfigurations) - set(z_vcpkg_tiff_link_libraries "") - set(z_vcpkg_tiff_libraries "") - if("@webp@") - find_package(WebP CONFIG ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${WebP_LIBRARIES}) - endif() - if("@lzma@") - find_package(LibLZMA ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${LIBLZMA_LIBRARIES}) - endif() - if("@jpeg@") - find_package(JPEG ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${JPEG_LIBRARIES}) - endif() - if("@zstd@") - find_package(zstd CONFIG ${z_vcpkg_tiff_find_options}) - set(z_vcpkg_tiff_zstd_target_property "IMPORTED_LOCATION_") - if(TARGET zstd::libzstd_shared) - set(z_vcpkg_tiff_zstd "\$") - set(z_vcpkg_tiff_zstd_target zstd::libzstd_shared) - if(WIN32) - set(z_vcpkg_tiff_zstd_target_property "IMPORTED_IMPLIB_") - endif() - else() - set(z_vcpkg_tiff_zstd "\$") - set(z_vcpkg_tiff_zstd_target zstd::libzstd_static) - endif() - get_target_property(z_vcpkg_tiff_zstd_configs "${z_vcpkg_tiff_zstd_target}" IMPORTED_CONFIGURATIONS) - foreach(z_vcpkg_config IN LISTS z_vcpkg_tiff_zstd_configs) - get_target_property(ZSTD_LIBRARY_${z_vcpkg_config} "${z_vcpkg_tiff_zstd_target}" "${z_vcpkg_tiff_zstd_target_property}${z_vcpkg_config}") - endforeach() - select_library_configurations(ZSTD) - if(NOT TARGET ZSTD::ZSTD) - add_library(ZSTD::ZSTD INTERFACE IMPORTED) - set_property(TARGET ZSTD::ZSTD APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${z_vcpkg_tiff_zstd}) - endif() - list(APPEND z_vcpkg_tiff_link_libraries ${z_vcpkg_tiff_zstd}) - list(APPEND z_vcpkg_tiff_libraries ${ZSTD_LIBRARIES}) - unset(z_vcpkg_tiff_zstd) - unset(z_vcpkg_tiff_zstd_configs) - unset(z_vcpkg_config) - unset(z_vcpkg_tiff_zstd_target) - endif() - if("@libdeflate@") - find_package(libdeflate ${z_vcpkg_tiff_find_options}) - set(z_vcpkg_property "IMPORTED_LOCATION_") - if(TARGET libdeflate::libdeflate_shared) - set(z_vcpkg_libdeflate_target libdeflate::libdeflate_shared) - if(WIN32) - set(z_vcpkg_property "IMPORTED_IMPLIB_") - endif() - else() - set(z_vcpkg_libdeflate_target libdeflate::libdeflate_static) - endif() - get_target_property(z_vcpkg_libdeflate_configs "${z_vcpkg_libdeflate_target}" IMPORTED_CONFIGURATIONS) - foreach(z_vcpkg_config IN LISTS z_vcpkg_libdeflate_configs) - get_target_property(Z_VCPKG_DEFLATE_LIBRARY_${z_vcpkg_config} "${z_vcpkg_libdeflate_target}" "${z_vcpkg_property}${z_vcpkg_config}") - endforeach() - select_library_configurations(Z_VCPKG_DEFLATE) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${Z_VCPKG_DEFLATE_LIBRARIES}) - unset(z_vcpkg_config) - unset(z_vcpkg_libdeflate_configs) - unset(z_vcpkg_libdeflate_target) - unset(z_vcpkg_property) - unset(Z_VCPKG_DEFLATE_FOUND) - endif() - if("@zlib@") - find_package(ZLIB ${z_vcpkg_tiff_find_options}) - list(APPEND z_vcpkg_tiff_link_libraries "\$") - list(APPEND z_vcpkg_tiff_libraries ${ZLIB_LIBRARIES}) - endif() - if(UNIX) - list(APPEND z_vcpkg_tiff_link_libraries m) - list(APPEND z_vcpkg_tiff_libraries m) - endif() - - if(TARGET TIFF::TIFF) - set_property(TARGET TIFF::TIFF APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${z_vcpkg_tiff_link_libraries}) - endif() - list(APPEND TIFF_LIBRARIES ${z_vcpkg_tiff_libraries}) - unset(z_vcpkg_tiff_link_libraries) - unset(z_vcpkg_tiff_libraries) -endif() -unset(z_vcpkg_tiff_find_options) -cmake_policy(POP) diff --git a/dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg.json b/dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg.json deleted file mode 100644 index 9b36e1a8..00000000 --- a/dependencies/vcpkg_overlay_ports_mac/tiff/vcpkg.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "tiff", - "version": "4.6.0", - "port-version": 2, - "description": "A library that supports the manipulation of TIFF image files", - "homepage": "https://libtiff.gitlab.io/libtiff/", - "license": "libtiff", - "dependencies": [ - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - } - ], - "default-features": [ - "jpeg", - "zip" - ], - "features": { - "cxx": { - "description": "Build C++ libtiffxx library" - }, - "jpeg": { - "description": "Support JPEG compression in TIFF image files", - "dependencies": [ - "libjpeg-turbo" - ] - }, - "libdeflate": { - "description": "Use libdeflate for faster ZIP support", - "dependencies": [ - "libdeflate", - { - "name": "tiff", - "default-features": false, - "features": [ - "zip" - ] - } - ] - }, - "tools": { - "description": "Build tools" - }, - "webp": { - "description": "Support WEBP compression in TIFF image files", - "dependencies": [ - "libwebp" - ] - }, - "zip": { - "description": "Support ZIP/deflate compression in TIFF image files", - "dependencies": [ - "zlib" - ] - }, - "zstd": { - "description": "Support ZSTD compression in TIFF image files", - "dependencies": [ - "zstd" - ] - } - } -} diff --git a/vcpkg.json b/vcpkg.json index b27a7095..0a46e32e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "name": "cemu", "version-string": "1.0", - "builtin-baseline": "cbf4a6641528cee6f172328984576f51698de726", + "builtin-baseline": "a4275b7eee79fb24ec2e135481ef5fce8b41c339", "dependencies": [ "pugixml", "zlib", @@ -44,6 +44,22 @@ "default-features": false, "features": [ "openssl" ] }, + { + "name": "dbus", + "default-features": false, + "platform": "linux" + }, + { + "name": "tiff", + "default-features": false, + "features": ["jpeg", "zip"] + }, "libusb" + ], + "overrides": [ + { + "name": "sdl2", + "version": "2.30.3" + } ] } From 1672f969bbc4a683e4a852aa2e145c1e6f9f68e6 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 9 Jun 2024 17:38:59 +0200 Subject: [PATCH 23/73] Latte: Add support for vertex format used by Rabbids Land --- .../LatteDecompilerEmitGLSLAttrDecoder.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSLAttrDecoder.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSLAttrDecoder.cpp index ea2b9491..76d76322 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSLAttrDecoder.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSLAttrDecoder.cpp @@ -241,6 +241,16 @@ void LatteDecompiler_emitAttributeDecodeGLSL(LatteDecompilerShader* shaderContex src->add("attrDecoder.z = floatBitsToUint(max(float(int(attrDecoder.z))/32767.0,-1.0));" _CRLF); src->add("attrDecoder.w = floatBitsToUint(max(float(int(attrDecoder.w))/32767.0,-1.0));" _CRLF); } + else if( attrib->format == FMT_16_16_16_16 && attrib->nfa == 2 && attrib->isSigned == 1 ) + { + // seen in Rabbids Land + _readLittleEndianAttributeU16x4(shaderContext, src, attributeInputIndex); + src->add("if( (attrDecoder.x&0x8000) != 0 ) attrDecoder.x |= 0xFFFF0000;" _CRLF); + src->add("if( (attrDecoder.y&0x8000) != 0 ) attrDecoder.y |= 0xFFFF0000;" _CRLF); + src->add("if( (attrDecoder.z&0x8000) != 0 ) attrDecoder.z |= 0xFFFF0000;" _CRLF); + src->add("if( (attrDecoder.w&0x8000) != 0 ) attrDecoder.w |= 0xFFFF0000;" _CRLF); + src->add("attrDecoder.xyzw = floatBitsToUint(vec4(ivec4(attrDecoder)));" _CRLF); + } else if (attrib->format == FMT_16_16_16_16_FLOAT && attrib->nfa == 2) { // seen in Giana Sisters: Twisted Dreams @@ -496,3 +506,5 @@ void LatteDecompiler_emitAttributeDecodeGLSL(LatteDecompilerShader* shaderContex cemu_assert_debug(false); } } + + From d4c2c3d2098616b3596b743f33fcc37629282ec0 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:50:06 +0200 Subject: [PATCH 24/73] nsyskbd: Stub KBDGetKey Fixes MSX VC games freezing on boot --- src/Cafe/OS/libs/nsyskbd/nsyskbd.cpp | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Cafe/OS/libs/nsyskbd/nsyskbd.cpp b/src/Cafe/OS/libs/nsyskbd/nsyskbd.cpp index 72cb9bd0..f1571cc0 100644 --- a/src/Cafe/OS/libs/nsyskbd/nsyskbd.cpp +++ b/src/Cafe/OS/libs/nsyskbd/nsyskbd.cpp @@ -3,6 +3,11 @@ namespace nsyskbd { + bool IsValidChannel(uint32 channel) + { + return channel >= 0 && channel < 4; + } + uint32 KBDGetChannelStatus(uint32 channel, uint32be* status) { static bool loggedError = false; @@ -16,8 +21,38 @@ namespace nsyskbd return 0; } +#pragma pack(push, 1) + struct KeyState + { + uint8be channel; + uint8be ukn1; + uint8be _padding[2]; + uint32be ukn4; + uint32be ukn8; + uint16be uknC; + }; +#pragma pack(pop) + static_assert(sizeof(KeyState) == 0xE); // actual size might be padded to 0x10? + + uint32 KBDGetKey(uint32 channel, KeyState* keyState) + { + // used by MSX VC + if(!IsValidChannel(channel) || !keyState) + { + cemuLog_log(LogType::APIErrors, "KBDGetKey(): Invalid parameter"); + return 0; + } + keyState->channel = channel; + keyState->ukn1 = 0; + keyState->ukn4 = 0; + keyState->ukn8 = 0; + keyState->uknC = 0; + return 0; + } + void nsyskbd_load() { cafeExportRegister("nsyskbd", KBDGetChannelStatus, LogType::Placeholder); + cafeExportRegister("nsyskbd", KBDGetKey, LogType::Placeholder); } } From f3d20832c191f90fe4ad6f9b64a3ff21eb477b02 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:28:21 +0200 Subject: [PATCH 25/73] Avoid an unhandled exception when mlc path is invalid --- src/gui/CemuApp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index 505a09c6..86d81e43 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -266,10 +266,10 @@ std::vector CemuApp::GetAvailableTranslationLanguages(wxT void CemuApp::CreateDefaultFiles(bool first_start) { + std::error_code ec; fs::path mlc = ActiveSettings::GetMlcPath(); - // check for mlc01 folder missing if custom path has been set - if (!fs::exists(mlc) && !first_start) + if (!fs::exists(mlc, ec) && !first_start) { const wxString message = formatWxString(_("Your mlc01 folder seems to be missing.\n\nThis is where Cemu stores save files, game updates and other Wii U files.\n\nThe expected path is:\n{}\n\nDo you want to create the folder at the expected path?"), _pathToUtf8(mlc)); From 93b58ae6f7315bf17126d49314e0132eeb356ef9 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Thu, 27 Jun 2024 23:55:20 +0100 Subject: [PATCH 26/73] nsyshid: Add infrastructure and support for emulating Skylander Portal (#971) --- src/Cafe/CMakeLists.txt | 4 + .../OS/libs/nsyshid/AttachDefaultBackends.cpp | 9 + src/Cafe/OS/libs/nsyshid/Backend.h | 57 +- src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp | 29 + src/Cafe/OS/libs/nsyshid/BackendEmulated.h | 16 + src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp | 36 +- src/Cafe/OS/libs/nsyshid/BackendLibusb.h | 6 +- .../OS/libs/nsyshid/BackendWindowsHID.cpp | 34 +- src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h | 6 +- src/Cafe/OS/libs/nsyshid/Skylander.cpp | 939 ++++++++++++++++++ src/Cafe/OS/libs/nsyshid/Skylander.h | 98 ++ src/Cafe/OS/libs/nsyshid/nsyshid.cpp | 41 +- src/config/CemuConfig.cpp | 8 + src/config/CemuConfig.h | 6 + src/gui/CMakeLists.txt | 2 + .../EmulatedUSBDeviceFrame.cpp | 354 +++++++ .../EmulatedUSBDeviceFrame.h | 42 + src/gui/MainWindow.cpp | 27 + src/gui/MainWindow.h | 2 + 19 files changed, 1658 insertions(+), 58 deletions(-) create mode 100644 src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp create mode 100644 src/Cafe/OS/libs/nsyshid/BackendEmulated.h create mode 100644 src/Cafe/OS/libs/nsyshid/Skylander.cpp create mode 100644 src/Cafe/OS/libs/nsyshid/Skylander.h create mode 100644 src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp create mode 100644 src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index b5090dcf..1583bdd7 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -457,10 +457,14 @@ add_library(CemuCafe OS/libs/nsyshid/AttachDefaultBackends.cpp OS/libs/nsyshid/Whitelist.cpp OS/libs/nsyshid/Whitelist.h + OS/libs/nsyshid/BackendEmulated.cpp + OS/libs/nsyshid/BackendEmulated.h OS/libs/nsyshid/BackendLibusb.cpp OS/libs/nsyshid/BackendLibusb.h OS/libs/nsyshid/BackendWindowsHID.cpp OS/libs/nsyshid/BackendWindowsHID.h + OS/libs/nsyshid/Skylander.cpp + OS/libs/nsyshid/Skylander.h OS/libs/nsyskbd/nsyskbd.cpp OS/libs/nsyskbd/nsyskbd.h OS/libs/nsysnet/nsysnet.cpp diff --git a/src/Cafe/OS/libs/nsyshid/AttachDefaultBackends.cpp b/src/Cafe/OS/libs/nsyshid/AttachDefaultBackends.cpp index 6e6cb123..fc8e496c 100644 --- a/src/Cafe/OS/libs/nsyshid/AttachDefaultBackends.cpp +++ b/src/Cafe/OS/libs/nsyshid/AttachDefaultBackends.cpp @@ -1,5 +1,6 @@ #include "nsyshid.h" #include "Backend.h" +#include "BackendEmulated.h" #if NSYSHID_ENABLE_BACKEND_LIBUSB @@ -37,5 +38,13 @@ namespace nsyshid::backend } } #endif // NSYSHID_ENABLE_BACKEND_WINDOWS_HID + // add emulated backend + { + auto backendEmulated = std::make_shared(); + if (backendEmulated->IsInitialisedOk()) + { + AttachBackend(backendEmulated); + } + } } } // namespace nsyshid::backend diff --git a/src/Cafe/OS/libs/nsyshid/Backend.h b/src/Cafe/OS/libs/nsyshid/Backend.h index 641104f5..03232736 100644 --- a/src/Cafe/OS/libs/nsyshid/Backend.h +++ b/src/Cafe/OS/libs/nsyshid/Backend.h @@ -23,6 +23,55 @@ namespace nsyshid /* +0x12 */ uint16be maxPacketSizeTX; } HID_t; + struct TransferCommand + { + uint8* data; + sint32 length; + + TransferCommand(uint8* data, sint32 length) + : data(data), length(length) + { + } + virtual ~TransferCommand() = default; + }; + + struct ReadMessage final : TransferCommand + { + sint32 bytesRead; + + ReadMessage(uint8* data, sint32 length, sint32 bytesRead) + : bytesRead(bytesRead), TransferCommand(data, length) + { + } + using TransferCommand::TransferCommand; + }; + + struct WriteMessage final : TransferCommand + { + sint32 bytesWritten; + + WriteMessage(uint8* data, sint32 length, sint32 bytesWritten) + : bytesWritten(bytesWritten), TransferCommand(data, length) + { + } + using TransferCommand::TransferCommand; + }; + + struct ReportMessage final : TransferCommand + { + uint8* reportData; + sint32 length; + uint8* originalData; + sint32 originalLength; + + ReportMessage(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) + : reportData(reportData), length(length), originalData(originalData), + originalLength(originalLength), TransferCommand(reportData, length) + { + } + using TransferCommand::TransferCommand; + }; + static_assert(offsetof(HID_t, vendorId) == 0x8, ""); static_assert(offsetof(HID_t, productId) == 0xA, ""); static_assert(offsetof(HID_t, ifIndex) == 0xC, ""); @@ -69,7 +118,7 @@ namespace nsyshid ErrorTimeout, }; - virtual ReadResult Read(uint8* data, sint32 length, sint32& bytesRead) = 0; + virtual ReadResult Read(ReadMessage* message) = 0; enum class WriteResult { @@ -78,7 +127,7 @@ namespace nsyshid ErrorTimeout, }; - virtual WriteResult Write(uint8* data, sint32 length, sint32& bytesWritten) = 0; + virtual WriteResult Write(WriteMessage* message) = 0; virtual bool GetDescriptor(uint8 descType, uint8 descIndex, @@ -88,7 +137,7 @@ namespace nsyshid virtual bool SetProtocol(uint32 ifIndef, uint32 protocol) = 0; - virtual bool SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) = 0; + virtual bool SetReport(ReportMessage* message) = 0; }; class Backend { @@ -121,6 +170,8 @@ namespace nsyshid std::shared_ptr FindDevice(std::function&)> isWantedDevice); + bool FindDeviceById(uint16 vendorId, uint16 productId); + bool IsDeviceWhitelisted(uint16 vendorId, uint16 productId); // called from OnAttach() - attach devices that your backend can see here diff --git a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp new file mode 100644 index 00000000..11a299ed --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp @@ -0,0 +1,29 @@ +#include "BackendEmulated.h" +#include "Skylander.h" +#include "config/CemuConfig.h" + +namespace nsyshid::backend::emulated +{ + BackendEmulated::BackendEmulated() + { + cemuLog_logDebug(LogType::Force, "nsyshid::BackendEmulated: emulated backend initialised"); + } + + BackendEmulated::~BackendEmulated() = default; + + bool BackendEmulated::IsInitialisedOk() + { + return true; + } + + void BackendEmulated::AttachVisibleDevices() + { + if (GetConfig().emulated_usb_devices.emulate_skylander_portal && !FindDeviceById(0x1430, 0x0150)) + { + cemuLog_logDebug(LogType::Force, "Attaching Emulated Portal"); + // Add Skylander Portal + 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/BackendEmulated.h b/src/Cafe/OS/libs/nsyshid/BackendEmulated.h new file mode 100644 index 00000000..cf38a8b7 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/BackendEmulated.h @@ -0,0 +1,16 @@ +#include "nsyshid.h" +#include "Backend.h" + +namespace nsyshid::backend::emulated +{ + class BackendEmulated : public nsyshid::Backend { + public: + BackendEmulated(); + ~BackendEmulated(); + + bool IsInitialisedOk() override; + + protected: + void AttachVisibleDevices() override; + }; +} // namespace nsyshid::backend::emulated diff --git a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp index 4f88b7ed..6701d780 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp @@ -241,11 +241,6 @@ namespace nsyshid::backend::libusb ret); return nullptr; } - if (desc.idVendor == 0x0e6f && desc.idProduct == 0x0241) - { - cemuLog_logDebug(LogType::Force, - "nsyshid::BackendLibusb::CheckAndCreateDevice(): lego dimensions portal detected"); - } auto device = std::make_shared(m_ctx, desc.idVendor, desc.idProduct, @@ -471,7 +466,7 @@ namespace nsyshid::backend::libusb return m_libusbHandle != nullptr && m_handleInUseCounter >= 0; } - Device::ReadResult DeviceLibusb::Read(uint8* data, sint32 length, sint32& bytesRead) + Device::ReadResult DeviceLibusb::Read(ReadMessage* message) { auto handleLock = AquireHandleLock(); if (!handleLock->IsValid()) @@ -488,8 +483,8 @@ namespace nsyshid::backend::libusb { ret = libusb_bulk_transfer(handleLock->GetHandle(), this->m_libusbEndpointIn, - data, - length, + message->data, + message->length, &actualLength, timeout); } @@ -500,8 +495,8 @@ namespace nsyshid::backend::libusb // success cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::read(): read {} of {} bytes", actualLength, - length); - bytesRead = actualLength; + message->length); + message->bytesRead = actualLength; return ReadResult::Success; } cemuLog_logDebug(LogType::Force, @@ -510,7 +505,7 @@ namespace nsyshid::backend::libusb return ReadResult::Error; } - Device::WriteResult DeviceLibusb::Write(uint8* data, sint32 length, sint32& bytesWritten) + Device::WriteResult DeviceLibusb::Write(WriteMessage* message) { auto handleLock = AquireHandleLock(); if (!handleLock->IsValid()) @@ -520,23 +515,23 @@ namespace nsyshid::backend::libusb return WriteResult::Error; } - bytesWritten = 0; + message->bytesWritten = 0; int actualLength = 0; int ret = libusb_bulk_transfer(handleLock->GetHandle(), this->m_libusbEndpointOut, - data, - length, + message->data, + message->length, &actualLength, 0); if (ret == 0) { // success - bytesWritten = actualLength; + message->bytesWritten = actualLength; cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::write(): wrote {} of {} bytes", - bytesWritten, - length); + message->bytesWritten, + message->length); return WriteResult::Success; } cemuLog_logDebug(LogType::Force, @@ -713,8 +708,7 @@ namespace nsyshid::backend::libusb return true; } - bool DeviceLibusb::SetReport(uint8* reportData, sint32 length, uint8* originalData, - sint32 originalLength) + bool DeviceLibusb::SetReport(ReportMessage* message) { auto handleLock = AquireHandleLock(); if (!handleLock->IsValid()) @@ -731,8 +725,8 @@ namespace nsyshid::backend::libusb bRequest, wValue, wIndex, - reportData, - length, + message->reportData, + message->length, timeout); #endif diff --git a/src/Cafe/OS/libs/nsyshid/BackendLibusb.h b/src/Cafe/OS/libs/nsyshid/BackendLibusb.h index 216be6ce..a8122aff 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendLibusb.h +++ b/src/Cafe/OS/libs/nsyshid/BackendLibusb.h @@ -63,9 +63,9 @@ namespace nsyshid::backend::libusb bool IsOpened() override; - ReadResult Read(uint8* data, sint32 length, sint32& bytesRead) override; + ReadResult Read(ReadMessage* message) override; - WriteResult Write(uint8* data, sint32 length, sint32& bytesWritten) override; + WriteResult Write(WriteMessage* message) override; bool GetDescriptor(uint8 descType, uint8 descIndex, @@ -75,7 +75,7 @@ namespace nsyshid::backend::libusb bool SetProtocol(uint32 ifIndex, uint32 protocol) override; - bool SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) override; + bool SetReport(ReportMessage* message) override; uint8 m_libusbBusNumber; uint8 m_libusbDeviceAddress; diff --git a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp index 23da5798..3cfba26a 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp @@ -196,20 +196,20 @@ namespace nsyshid::backend::windows return m_hFile != INVALID_HANDLE_VALUE; } - Device::ReadResult DeviceWindowsHID::Read(uint8* data, sint32 length, sint32& bytesRead) + Device::ReadResult DeviceWindowsHID::Read(ReadMessage* message) { - bytesRead = 0; + message->bytesRead = 0; DWORD bt; OVERLAPPED ovlp = {0}; ovlp.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); - uint8* tempBuffer = (uint8*)malloc(length + 1); + uint8* tempBuffer = (uint8*)malloc(message->length + 1); sint32 transferLength = 0; // minus report byte - _debugPrintHex("HID_READ_BEFORE", data, length); + _debugPrintHex("HID_READ_BEFORE", message->data, message->length); - cemuLog_logDebug(LogType::Force, "HidRead Begin (Length 0x{:08x})", length); - BOOL readResult = ReadFile(this->m_hFile, tempBuffer, length + 1, &bt, &ovlp); + cemuLog_logDebug(LogType::Force, "HidRead Begin (Length 0x{:08x})", message->length); + BOOL readResult = ReadFile(this->m_hFile, tempBuffer, message->length + 1, &bt, &ovlp); if (readResult != FALSE) { // sometimes we get the result immediately @@ -247,7 +247,7 @@ namespace nsyshid::backend::windows ReadResult result = ReadResult::Success; if (bt != 0) { - memcpy(data, tempBuffer + 1, transferLength); + memcpy(message->data, tempBuffer + 1, transferLength); sint32 hidReadLength = transferLength; char debugOutput[1024] = {0}; @@ -257,7 +257,7 @@ namespace nsyshid::backend::windows } cemuLog_logDebug(LogType::Force, "HIDRead data: {}", debugOutput); - bytesRead = transferLength; + message->bytesRead = transferLength; result = ReadResult::Success; } else @@ -270,19 +270,19 @@ namespace nsyshid::backend::windows return result; } - Device::WriteResult DeviceWindowsHID::Write(uint8* data, sint32 length, sint32& bytesWritten) + Device::WriteResult DeviceWindowsHID::Write(WriteMessage* message) { - bytesWritten = 0; + message->bytesWritten = 0; DWORD bt; OVERLAPPED ovlp = {0}; ovlp.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); - uint8* tempBuffer = (uint8*)malloc(length + 1); - memcpy(tempBuffer + 1, data, length); + uint8* tempBuffer = (uint8*)malloc(message->length + 1); + memcpy(tempBuffer + 1, message->data, message->length); tempBuffer[0] = 0; // report byte? - cemuLog_logDebug(LogType::Force, "HidWrite Begin (Length 0x{:08x})", length); - BOOL writeResult = WriteFile(this->m_hFile, tempBuffer, length + 1, &bt, &ovlp); + cemuLog_logDebug(LogType::Force, "HidWrite Begin (Length 0x{:08x})", message->length); + BOOL writeResult = WriteFile(this->m_hFile, tempBuffer, message->length + 1, &bt, &ovlp); if (writeResult != FALSE) { // sometimes we get the result immediately @@ -314,7 +314,7 @@ namespace nsyshid::backend::windows if (bt != 0) { - bytesWritten = length; + message->bytesWritten = message->length; return WriteResult::Success; } return WriteResult::Error; @@ -407,12 +407,12 @@ namespace nsyshid::backend::windows return true; } - bool DeviceWindowsHID::SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) + bool DeviceWindowsHID::SetReport(ReportMessage* message) { sint32 retryCount = 0; while (true) { - BOOL r = HidD_SetOutputReport(this->m_hFile, reportData, length); + BOOL r = HidD_SetOutputReport(this->m_hFile, message->reportData, message->length); if (r != FALSE) break; Sleep(20); // retry diff --git a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h index 049b33e4..84fe7bda 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h +++ b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h @@ -41,15 +41,15 @@ namespace nsyshid::backend::windows bool IsOpened() override; - ReadResult Read(uint8* data, sint32 length, sint32& bytesRead) override; + ReadResult Read(ReadMessage* message) override; - WriteResult Write(uint8* data, sint32 length, sint32& bytesWritten) override; + WriteResult Write(WriteMessage* message) override; bool GetDescriptor(uint8 descType, uint8 descIndex, uint8 lang, uint8* output, uint32 outputMaxLength) override; bool SetProtocol(uint32 ifIndef, uint32 protocol) override; - bool SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) override; + bool SetReport(ReportMessage* message) override; private: wchar_t* m_devicePath; diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp new file mode 100644 index 00000000..3123d14d --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -0,0 +1,939 @@ +#include "Skylander.h" + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +namespace nsyshid +{ + SkylanderUSB g_skyportal; + + const std::map, const std::string> + listSkylanders = { + {{0, 0x0000}, "Whirlwind"}, + {{0, 0x1801}, "Series 2 Whirlwind"}, + {{0, 0x1C02}, "Polar Whirlwind"}, + {{0, 0x2805}, "Horn Blast Whirlwind"}, + {{0, 0x3810}, "Eon's Elite Whirlwind"}, + {{1, 0x0000}, "Sonic Boom"}, + {{1, 0x1801}, "Series 2 Sonic Boom"}, + {{2, 0x0000}, "Warnado"}, + {{2, 0x2206}, "LightCore Warnado"}, + {{3, 0x0000}, "Lightning Rod"}, + {{3, 0x1801}, "Series 2 Lightning Rod"}, + {{4, 0x0000}, "Bash"}, + {{4, 0x1801}, "Series 2 Bash"}, + {{5, 0x0000}, "Terrafin"}, + {{5, 0x1801}, "Series 2 Terrafin"}, + {{5, 0x2805}, "Knockout Terrafin"}, + {{5, 0x3810}, "Eon's Elite Terrafin"}, + {{6, 0x0000}, "Dino Rang"}, + {{6, 0x4810}, "Eon's Elite Dino Rang"}, + {{7, 0x0000}, "Prism Break"}, + {{7, 0x1801}, "Series 2 Prism Break"}, + {{7, 0x2805}, "Hyper Beam Prism Break"}, + {{7, 0x1206}, "LightCore Prism Break"}, + {{8, 0x0000}, "Sunburn"}, + {{9, 0x0000}, "Eruptor"}, + {{9, 0x1801}, "Series 2 Eruptor"}, + {{9, 0x2C02}, "Volcanic Eruptor"}, + {{9, 0x2805}, "Lava Barf Eruptor"}, + {{9, 0x1206}, "LightCore Eruptor"}, + {{9, 0x3810}, "Eon's Elite Eruptor"}, + {{10, 0x0000}, "Ignitor"}, + {{10, 0x1801}, "Series 2 Ignitor"}, + {{10, 0x1C03}, "Legendary Ignitor"}, + {{11, 0x0000}, "Flameslinger"}, + {{11, 0x1801}, "Series 2 Flameslinger"}, + {{12, 0x0000}, "Zap"}, + {{12, 0x1801}, "Series 2 Zap"}, + {{13, 0x0000}, "Wham Shell"}, + {{13, 0x2206}, "LightCore Wham Shell"}, + {{14, 0x0000}, "Gill Grunt"}, + {{14, 0x1801}, "Series 2 Gill Grunt"}, + {{14, 0x2805}, "Anchors Away Gill Grunt"}, + {{14, 0x3805}, "Tidal Wave Gill Grunt"}, + {{14, 0x3810}, "Eon's Elite Gill Grunt"}, + {{15, 0x0000}, "Slam Bam"}, + {{15, 0x1801}, "Series 2 Slam Bam"}, + {{15, 0x1C03}, "Legendary Slam Bam"}, + {{15, 0x4810}, "Eon's Elite Slam Bam"}, + {{16, 0x0000}, "Spyro"}, + {{16, 0x1801}, "Series 2 Spyro"}, + {{16, 0x2C02}, "Dark Mega Ram Spyro"}, + {{16, 0x2805}, "Mega Ram Spyro"}, + {{16, 0x3810}, "Eon's Elite Spyro"}, + {{17, 0x0000}, "Voodood"}, + {{17, 0x4810}, "Eon's Elite Voodood"}, + {{18, 0x0000}, "Double Trouble"}, + {{18, 0x1801}, "Series 2 Double Trouble"}, + {{18, 0x1C02}, "Royal Double Trouble"}, + {{19, 0x0000}, "Trigger Happy"}, + {{19, 0x1801}, "Series 2 Trigger Happy"}, + {{19, 0x2C02}, "Springtime Trigger Happy"}, + {{19, 0x2805}, "Big Bang Trigger Happy"}, + {{19, 0x3810}, "Eon's Elite Trigger Happy"}, + {{20, 0x0000}, "Drobot"}, + {{20, 0x1801}, "Series 2 Drobot"}, + {{20, 0x1206}, "LightCore Drobot"}, + {{21, 0x0000}, "Drill Seargeant"}, + {{21, 0x1801}, "Series 2 Drill Seargeant"}, + {{22, 0x0000}, "Boomer"}, + {{22, 0x4810}, "Eon's Elite Boomer"}, + {{23, 0x0000}, "Wrecking Ball"}, + {{23, 0x1801}, "Series 2 Wrecking Ball"}, + {{24, 0x0000}, "Camo"}, + {{24, 0x2805}, "Thorn Horn Camo"}, + {{25, 0x0000}, "Zook"}, + {{25, 0x1801}, "Series 2 Zook"}, + {{25, 0x4810}, "Eon's Elite Zook"}, + {{26, 0x0000}, "Stealth Elf"}, + {{26, 0x1801}, "Series 2 Stealth Elf"}, + {{26, 0x2C02}, "Dark Stealth Elf"}, + {{26, 0x1C03}, "Legendary Stealth Elf"}, + {{26, 0x2805}, "Ninja Stealth Elf"}, + {{26, 0x3810}, "Eon's Elite Stealth Elf"}, + {{27, 0x0000}, "Stump Smash"}, + {{27, 0x1801}, "Series 2 Stump Smash"}, + {{28, 0x0000}, "Dark Spyro"}, + {{29, 0x0000}, "Hex"}, + {{29, 0x1801}, "Series 2 Hex"}, + {{29, 0x1206}, "LightCore Hex"}, + {{30, 0x0000}, "Chop Chop"}, + {{30, 0x1801}, "Series 2 Chop Chop"}, + {{30, 0x2805}, "Twin Blade Chop Chop"}, + {{30, 0x3810}, "Eon's Elite Chop Chop"}, + {{31, 0x0000}, "Ghost Roaster"}, + {{31, 0x4810}, "Eon's Elite Ghost Roaster"}, + {{32, 0x0000}, "Cynder"}, + {{32, 0x1801}, "Series 2 Cynder"}, + {{32, 0x2805}, "Phantom Cynder"}, + {{100, 0x0000}, "Jet Vac"}, + {{100, 0x1403}, "Legendary Jet Vac"}, + {{100, 0x2805}, "Turbo Jet Vac"}, + {{100, 0x3805}, "Full Blast Jet Vac"}, + {{100, 0x1206}, "LightCore Jet Vac"}, + {{101, 0x0000}, "Swarm"}, + {{102, 0x0000}, "Crusher"}, + {{102, 0x1602}, "Granite Crusher"}, + {{103, 0x0000}, "Flashwing"}, + {{103, 0x1402}, "Jade Flash Wing"}, + {{103, 0x2206}, "LightCore Flashwing"}, + {{104, 0x0000}, "Hot Head"}, + {{105, 0x0000}, "Hot Dog"}, + {{105, 0x1402}, "Molten Hot Dog"}, + {{105, 0x2805}, "Fire Bone Hot Dog"}, + {{106, 0x0000}, "Chill"}, + {{106, 0x1603}, "Legendary Chill"}, + {{106, 0x2805}, "Blizzard Chill"}, + {{106, 0x1206}, "LightCore Chill"}, + {{107, 0x0000}, "Thumpback"}, + {{108, 0x0000}, "Pop Fizz"}, + {{108, 0x1402}, "Punch Pop Fizz"}, + {{108, 0x3C02}, "Love Potion Pop Fizz"}, + {{108, 0x2805}, "Super Gulp Pop Fizz"}, + {{108, 0x3805}, "Fizzy Frenzy Pop Fizz"}, + {{108, 0x1206}, "LightCore Pop Fizz"}, + {{109, 0x0000}, "Ninjini"}, + {{109, 0x1602}, "Scarlet Ninjini"}, + {{110, 0x0000}, "Bouncer"}, + {{110, 0x1603}, "Legendary Bouncer"}, + {{111, 0x0000}, "Sprocket"}, + {{111, 0x2805}, "Heavy Duty Sprocket"}, + {{112, 0x0000}, "Tree Rex"}, + {{112, 0x1602}, "Gnarly Tree Rex"}, + {{113, 0x0000}, "Shroomboom"}, + {{113, 0x3805}, "Sure Shot Shroomboom"}, + {{113, 0x1206}, "LightCore Shroomboom"}, + {{114, 0x0000}, "Eye Brawl"}, + {{115, 0x0000}, "Fright Rider"}, + {{200, 0x0000}, "Anvil Rain"}, + {{201, 0x0000}, "Hidden Treasure"}, + {{201, 0x2000}, "Platinum Hidden Treasure"}, + {{202, 0x0000}, "Healing Elixir"}, + {{203, 0x0000}, "Ghost Pirate Swords"}, + {{204, 0x0000}, "Time Twist Hourglass"}, + {{205, 0x0000}, "Sky Iron Shield"}, + {{206, 0x0000}, "Winged Boots"}, + {{207, 0x0000}, "Sparx the Dragonfly"}, + {{208, 0x0000}, "Dragonfire Cannon"}, + {{208, 0x1602}, "Golden Dragonfire Cannon"}, + {{209, 0x0000}, "Scorpion Striker"}, + {{210, 0x3002}, "Biter's Bane"}, + {{210, 0x3008}, "Sorcerous Skull"}, + {{210, 0x300B}, "Axe of Illusion"}, + {{210, 0x300E}, "Arcane Hourglass"}, + {{210, 0x3012}, "Spell Slapper"}, + {{210, 0x3014}, "Rune Rocket"}, + {{211, 0x3001}, "Tidal Tiki"}, + {{211, 0x3002}, "Wet Walter"}, + {{211, 0x3006}, "Flood Flask"}, + {{211, 0x3406}, "Legendary Flood Flask"}, + {{211, 0x3007}, "Soaking Staff"}, + {{211, 0x300B}, "Aqua Axe"}, + {{211, 0x3016}, "Frost Helm"}, + {{212, 0x3003}, "Breezy Bird"}, + {{212, 0x3006}, "Drafty Decanter"}, + {{212, 0x300D}, "Tempest Timer"}, + {{212, 0x3010}, "Cloudy Cobra"}, + {{212, 0x3011}, "Storm Warning"}, + {{212, 0x3018}, "Cyclone Saber"}, + {{213, 0x3004}, "Spirit Sphere"}, + {{213, 0x3404}, "Legendary Spirit Sphere"}, + {{213, 0x3008}, "Spectral Skull"}, + {{213, 0x3408}, "Legendary Spectral Skull"}, + {{213, 0x300B}, "Haunted Hatchet"}, + {{213, 0x300C}, "Grim Gripper"}, + {{213, 0x3010}, "Spooky Snake"}, + {{213, 0x3017}, "Dream Piercer"}, + {{214, 0x3000}, "Tech Totem"}, + {{214, 0x3007}, "Automatic Angel"}, + {{214, 0x3009}, "Factory Flower"}, + {{214, 0x300C}, "Grabbing Gadget"}, + {{214, 0x3016}, "Makers Mana"}, + {{214, 0x301A}, "Topsy Techy"}, + {{215, 0x3005}, "Eternal Flame"}, + {{215, 0x3009}, "Fire Flower"}, + {{215, 0x3011}, "Scorching Stopper"}, + {{215, 0x3012}, "Searing Spinner"}, + {{215, 0x3017}, "Spark Spear"}, + {{215, 0x301B}, "Blazing Belch"}, + {{216, 0x3000}, "Banded Boulder"}, + {{216, 0x3003}, "Rock Hawk"}, + {{216, 0x300A}, "Slag Hammer"}, + {{216, 0x300E}, "Dust Of Time"}, + {{216, 0x3013}, "Spinning Sandstorm"}, + {{216, 0x301A}, "Rubble Trouble"}, + {{217, 0x3003}, "Oak Eagle"}, + {{217, 0x3005}, "Emerald Energy"}, + {{217, 0x300A}, "Weed Whacker"}, + {{217, 0x3010}, "Seed Serpent"}, + {{217, 0x3018}, "Jade Blade"}, + {{217, 0x301B}, "Shrub Shrieker"}, + {{218, 0x3000}, "Dark Dagger"}, + {{218, 0x3014}, "Shadow Spider"}, + {{218, 0x301A}, "Ghastly Grimace"}, + {{219, 0x3000}, "Shining Ship"}, + {{219, 0x300F}, "Heavenly Hawk"}, + {{219, 0x301B}, "Beam Scream"}, + {{220, 0x301E}, "Kaos Trap"}, + {{220, 0x351F}, "Ultimate Kaos Trap"}, + {{230, 0x0000}, "Hand of Fate"}, + {{230, 0x3403}, "Legendary Hand of Fate"}, + {{231, 0x0000}, "Piggy Bank"}, + {{232, 0x0000}, "Rocket Ram"}, + {{233, 0x0000}, "Tiki Speaky"}, + {{300, 0x0000}, "Dragon’s Peak"}, + {{301, 0x0000}, "Empire of Ice"}, + {{302, 0x0000}, "Pirate Seas"}, + {{303, 0x0000}, "Darklight Crypt"}, + {{304, 0x0000}, "Volcanic Vault"}, + {{305, 0x0000}, "Mirror of Mystery"}, + {{306, 0x0000}, "Nightmare Express"}, + {{307, 0x0000}, "Sunscraper Spire"}, + {{308, 0x0000}, "Midnight Museum"}, + {{404, 0x0000}, "Legendary Bash"}, + {{416, 0x0000}, "Legendary Spyro"}, + {{419, 0x0000}, "Legendary Trigger Happy"}, + {{430, 0x0000}, "Legendary Chop Chop"}, + {{450, 0x0000}, "Gusto"}, + {{451, 0x0000}, "Thunderbolt"}, + {{452, 0x0000}, "Fling Kong"}, + {{453, 0x0000}, "Blades"}, + {{453, 0x3403}, "Legendary Blades"}, + {{454, 0x0000}, "Wallop"}, + {{455, 0x0000}, "Head Rush"}, + {{455, 0x3402}, "Nitro Head Rush"}, + {{456, 0x0000}, "Fist Bump"}, + {{457, 0x0000}, "Rocky Roll"}, + {{458, 0x0000}, "Wildfire"}, + {{458, 0x3402}, "Dark Wildfire"}, + {{459, 0x0000}, "Ka Boom"}, + {{460, 0x0000}, "Trail Blazer"}, + {{461, 0x0000}, "Torch"}, + {{462, 0x3000}, "Snap Shot"}, + {{462, 0x3402}, "Dark Snap Shot"}, + {{463, 0x0000}, "Lob Star"}, + {{463, 0x3402}, "Winterfest Lob-Star"}, + {{464, 0x0000}, "Flip Wreck"}, + {{465, 0x0000}, "Echo"}, + {{466, 0x0000}, "Blastermind"}, + {{467, 0x0000}, "Enigma"}, + {{468, 0x0000}, "Deja Vu"}, + {{468, 0x3403}, "Legendary Deja Vu"}, + {{469, 0x0000}, "Cobra Candabra"}, + {{469, 0x3402}, "King Cobra Cadabra"}, + {{470, 0x0000}, "Jawbreaker"}, + {{470, 0x3403}, "Legendary Jawbreaker"}, + {{471, 0x0000}, "Gearshift"}, + {{472, 0x0000}, "Chopper"}, + {{473, 0x0000}, "Tread Head"}, + {{474, 0x0000}, "Bushwack"}, + {{474, 0x3403}, "Legendary Bushwack"}, + {{475, 0x0000}, "Tuff Luck"}, + {{476, 0x0000}, "Food Fight"}, + {{476, 0x3402}, "Dark Food Fight"}, + {{477, 0x0000}, "High Five"}, + {{478, 0x0000}, "Krypt King"}, + {{478, 0x3402}, "Nitro Krypt King"}, + {{479, 0x0000}, "Short Cut"}, + {{480, 0x0000}, "Bat Spin"}, + {{481, 0x0000}, "Funny Bone"}, + {{482, 0x0000}, "Knight Light"}, + {{483, 0x0000}, "Spotlight"}, + {{484, 0x0000}, "Knight Mare"}, + {{485, 0x0000}, "Blackout"}, + {{502, 0x0000}, "Bop"}, + {{505, 0x0000}, "Terrabite"}, + {{506, 0x0000}, "Breeze"}, + {{508, 0x0000}, "Pet Vac"}, + {{508, 0x3402}, "Power Punch Pet Vac"}, + {{507, 0x0000}, "Weeruptor"}, + {{507, 0x3402}, "Eggcellent Weeruptor"}, + {{509, 0x0000}, "Small Fry"}, + {{510, 0x0000}, "Drobit"}, + {{519, 0x0000}, "Trigger Snappy"}, + {{526, 0x0000}, "Whisper Elf"}, + {{540, 0x0000}, "Barkley"}, + {{540, 0x3402}, "Gnarly Barkley"}, + {{541, 0x0000}, "Thumpling"}, + {{514, 0x0000}, "Gill Runt"}, + {{542, 0x0000}, "Mini-Jini"}, + {{503, 0x0000}, "Spry"}, + {{504, 0x0000}, "Hijinx"}, + {{543, 0x0000}, "Eye Small"}, + {{601, 0x0000}, "King Pen"}, + {{602, 0x0000}, "Tri-Tip"}, + {{603, 0x0000}, "Chopscotch"}, + {{604, 0x0000}, "Boom Bloom"}, + {{605, 0x0000}, "Pit Boss"}, + {{606, 0x0000}, "Barbella"}, + {{607, 0x0000}, "Air Strike"}, + {{608, 0x0000}, "Ember"}, + {{609, 0x0000}, "Ambush"}, + {{610, 0x0000}, "Dr. Krankcase"}, + {{611, 0x0000}, "Hood Sickle"}, + {{612, 0x0000}, "Tae Kwon Crow"}, + {{613, 0x0000}, "Golden Queen"}, + {{614, 0x0000}, "Wolfgang"}, + {{615, 0x0000}, "Pain-Yatta"}, + {{616, 0x0000}, "Mysticat"}, + {{617, 0x0000}, "Starcast"}, + {{618, 0x0000}, "Buckshot"}, + {{619, 0x0000}, "Aurora"}, + {{620, 0x0000}, "Flare Wolf"}, + {{621, 0x0000}, "Chompy Mage"}, + {{622, 0x0000}, "Bad Juju"}, + {{623, 0x0000}, "Grave Clobber"}, + {{624, 0x0000}, "Blaster-Tron"}, + {{625, 0x0000}, "Ro-Bow"}, + {{626, 0x0000}, "Chain Reaction"}, + {{627, 0x0000}, "Kaos"}, + {{628, 0x0000}, "Wild Storm"}, + {{629, 0x0000}, "Tidepool"}, + {{630, 0x0000}, "Crash Bandicoot"}, + {{631, 0x0000}, "Dr. Neo Cortex"}, + {{1000, 0x0000}, "Boom Jet (Bottom)"}, + {{1001, 0x0000}, "Free Ranger (Bottom)"}, + {{1001, 0x2403}, "Legendary Free Ranger (Bottom)"}, + {{1002, 0x0000}, "Rubble Rouser (Bottom)"}, + {{1003, 0x0000}, "Doom Stone (Bottom)"}, + {{1004, 0x0000}, "Blast Zone (Bottom)"}, + {{1004, 0x2402}, "Dark Blast Zone (Bottom)"}, + {{1005, 0x0000}, "Fire Kraken (Bottom)"}, + {{1005, 0x2402}, "Jade Fire Kraken (Bottom)"}, + {{1006, 0x0000}, "Stink Bomb (Bottom)"}, + {{1007, 0x0000}, "Grilla Drilla (Bottom)"}, + {{1008, 0x0000}, "Hoot Loop (Bottom)"}, + {{1008, 0x2402}, "Enchanted Hoot Loop (Bottom)"}, + {{1009, 0x0000}, "Trap Shadow (Bottom)"}, + {{1010, 0x0000}, "Magna Charge (Bottom)"}, + {{1010, 0x2402}, "Nitro Magna Charge (Bottom)"}, + {{1011, 0x0000}, "Spy Rise (Bottom)"}, + {{1012, 0x0000}, "Night Shift (Bottom)"}, + {{1012, 0x2403}, "Legendary Night Shift (Bottom)"}, + {{1013, 0x0000}, "Rattle Shake (Bottom)"}, + {{1013, 0x2402}, "Quick Draw Rattle Shake (Bottom)"}, + {{1014, 0x0000}, "Freeze Blade (Bottom)"}, + {{1014, 0x2402}, "Nitro Freeze Blade (Bottom)"}, + {{1015, 0x0000}, "Wash Buckler (Bottom)"}, + {{1015, 0x2402}, "Dark Wash Buckler (Bottom)"}, + {{2000, 0x0000}, "Boom Jet (Top)"}, + {{2001, 0x0000}, "Free Ranger (Top)"}, + {{2001, 0x2403}, "Legendary Free Ranger (Top)"}, + {{2002, 0x0000}, "Rubble Rouser (Top)"}, + {{2003, 0x0000}, "Doom Stone (Top)"}, + {{2004, 0x0000}, "Blast Zone (Top)"}, + {{2004, 0x2402}, "Dark Blast Zone (Top)"}, + {{2005, 0x0000}, "Fire Kraken (Top)"}, + {{2005, 0x2402}, "Jade Fire Kraken (Top)"}, + {{2006, 0x0000}, "Stink Bomb (Top)"}, + {{2007, 0x0000}, "Grilla Drilla (Top)"}, + {{2008, 0x0000}, "Hoot Loop (Top)"}, + {{2008, 0x2402}, "Enchanted Hoot Loop (Top)"}, + {{2009, 0x0000}, "Trap Shadow (Top)"}, + {{2010, 0x0000}, "Magna Charge (Top)"}, + {{2010, 0x2402}, "Nitro Magna Charge (Top)"}, + {{2011, 0x0000}, "Spy Rise (Top)"}, + {{2012, 0x0000}, "Night Shift (Top)"}, + {{2012, 0x2403}, "Legendary Night Shift (Top)"}, + {{2013, 0x0000}, "Rattle Shake (Top)"}, + {{2013, 0x2402}, "Quick Draw Rattle Shake (Top)"}, + {{2014, 0x0000}, "Freeze Blade (Top)"}, + {{2014, 0x2402}, "Nitro Freeze Blade (Top)"}, + {{2015, 0x0000}, "Wash Buckler (Top)"}, + {{2015, 0x2402}, "Dark Wash Buckler (Top)"}, + {{3000, 0x0000}, "Scratch"}, + {{3001, 0x0000}, "Pop Thorn"}, + {{3002, 0x0000}, "Slobber Tooth"}, + {{3002, 0x2402}, "Dark Slobber Tooth"}, + {{3003, 0x0000}, "Scorp"}, + {{3004, 0x0000}, "Fryno"}, + {{3004, 0x3805}, "Hog Wild Fryno"}, + {{3005, 0x0000}, "Smolderdash"}, + {{3005, 0x2206}, "LightCore Smolderdash"}, + {{3006, 0x0000}, "Bumble Blast"}, + {{3006, 0x2402}, "Jolly Bumble Blast"}, + {{3006, 0x2206}, "LightCore Bumble Blast"}, + {{3007, 0x0000}, "Zoo Lou"}, + {{3007, 0x2403}, "Legendary Zoo Lou"}, + {{3008, 0x0000}, "Dune Bug"}, + {{3009, 0x0000}, "Star Strike"}, + {{3009, 0x2602}, "Enchanted Star Strike"}, + {{3009, 0x2206}, "LightCore Star Strike"}, + {{3010, 0x0000}, "Countdown"}, + {{3010, 0x2402}, "Kickoff Countdown"}, + {{3010, 0x2206}, "LightCore Countdown"}, + {{3011, 0x0000}, "Wind Up"}, + {{3011, 0x2404}, "Gear Head VVind Up"}, + {{3012, 0x0000}, "Roller Brawl"}, + {{3013, 0x0000}, "Grim Creeper"}, + {{3013, 0x2603}, "Legendary Grim Creeper"}, + {{3013, 0x2206}, "LightCore Grim Creeper"}, + {{3014, 0x0000}, "Rip Tide"}, + {{3015, 0x0000}, "Punk Shock"}, + {{3200, 0x0000}, "Battle Hammer"}, + {{3201, 0x0000}, "Sky Diamond"}, + {{3202, 0x0000}, "Platinum Sheep"}, + {{3203, 0x0000}, "Groove Machine"}, + {{3204, 0x0000}, "UFO Hat"}, + {{3300, 0x0000}, "Sheep Wreck Island"}, + {{3301, 0x0000}, "Tower of Time"}, + {{3302, 0x0000}, "Fiery Forge"}, + {{3303, 0x0000}, "Arkeyan Crossbow"}, + {{3220, 0x0000}, "Jet Stream"}, + {{3221, 0x0000}, "Tomb Buggy"}, + {{3222, 0x0000}, "Reef Ripper"}, + {{3223, 0x0000}, "Burn Cycle"}, + {{3224, 0x0000}, "Hot Streak"}, + {{3224, 0x4402}, "Dark Hot Streak"}, + {{3224, 0x4004}, "E3 Hot Streak"}, + {{3224, 0x441E}, "Golden Hot Streak"}, + {{3225, 0x0000}, "Shark Tank"}, + {{3226, 0x0000}, "Thump Truck"}, + {{3227, 0x0000}, "Crypt Crusher"}, + {{3228, 0x0000}, "Stealth Stinger"}, + {{3228, 0x4402}, "Nitro Stealth Stinger"}, + {{3231, 0x0000}, "Dive Bomber"}, + {{3231, 0x4402}, "Spring Ahead Dive Bomber"}, + {{3232, 0x0000}, "Sky Slicer"}, + {{3233, 0x0000}, "Clown Cruiser (Nintendo Only)"}, + {{3233, 0x4402}, "Dark Clown Cruiser (Nintendo Only)"}, + {{3234, 0x0000}, "Gold Rusher"}, + {{3234, 0x4402}, "Power Blue Gold Rusher"}, + {{3235, 0x0000}, "Shield Striker"}, + {{3236, 0x0000}, "Sun Runner"}, + {{3236, 0x4403}, "Legendary Sun Runner"}, + {{3237, 0x0000}, "Sea Shadow"}, + {{3237, 0x4402}, "Dark Sea Shadow"}, + {{3238, 0x0000}, "Splatter Splasher"}, + {{3238, 0x4402}, "Power Blue Splatter Splasher"}, + {{3239, 0x0000}, "Soda Skimmer"}, + {{3239, 0x4402}, "Nitro Soda Skimmer"}, + {{3240, 0x0000}, "Barrel Blaster (Nintendo Only)"}, + {{3240, 0x4402}, "Dark Barrel Blaster (Nintendo Only)"}, + {{3241, 0x0000}, "Buzz Wing"}, + {{3400, 0x0000}, "Fiesta"}, + {{3400, 0x4515}, "Frightful Fiesta"}, + {{3401, 0x0000}, "High Volt"}, + {{3402, 0x0000}, "Splat"}, + {{3402, 0x4502}, "Power Blue Splat"}, + {{3406, 0x0000}, "Stormblade"}, + {{3411, 0x0000}, "Smash Hit"}, + {{3411, 0x4502}, "Steel Plated Smash Hit"}, + {{3412, 0x0000}, "Spitfire"}, + {{3412, 0x4502}, "Dark Spitfire"}, + {{3413, 0x0000}, "Hurricane Jet Vac"}, + {{3413, 0x4503}, "Legendary Hurricane Jet Vac"}, + {{3414, 0x0000}, "Double Dare Trigger Happy"}, + {{3414, 0x4502}, "Power Blue Double Dare Trigger Happy"}, + {{3415, 0x0000}, "Super Shot Stealth Elf"}, + {{3415, 0x4502}, "Dark Super Shot Stealth Elf"}, + {{3416, 0x0000}, "Shark Shooter Terrafin"}, + {{3417, 0x0000}, "Bone Bash Roller Brawl"}, + {{3417, 0x4503}, "Legendary Bone Bash Roller Brawl"}, + {{3420, 0x0000}, "Big Bubble Pop Fizz"}, + {{3420, 0x450E}, "Birthday Bash Big Bubble Pop Fizz"}, + {{3421, 0x0000}, "Lava Lance Eruptor"}, + {{3422, 0x0000}, "Deep Dive Gill Grunt"}, + {{3423, 0x0000}, "Turbo Charge Donkey Kong (Nintendo Only)"}, + {{3423, 0x4502}, "Dark Turbo Charge Donkey Kong (Nintendo Only)"}, + {{3424, 0x0000}, "Hammer Slam Bowser (Nintendo Only)"}, + {{3424, 0x4502}, "Dark Hammer Slam Bowser (Nintendo Only)"}, + {{3425, 0x0000}, "Dive-Clops"}, + {{3425, 0x450E}, "Missile-Tow Dive-Clops"}, + {{3426, 0x0000}, "Astroblast"}, + {{3426, 0x4503}, "Legendary Astroblast"}, + {{3427, 0x0000}, "Nightfall"}, + {{3428, 0x0000}, "Thrillipede"}, + {{3428, 0x450D}, "Eggcited Thrillipede"}, + {{3500, 0x0000}, "Sky Trophy"}, + {{3501, 0x0000}, "Land Trophy"}, + {{3502, 0x0000}, "Sea Trophy"}, + {{3503, 0x0000}, "Kaos Trophy"}, + }; + + uint16 SkylanderUSB::SkylanderCRC16(uint16 initValue, const uint8* buffer, uint32 size) + { + const unsigned short CRC_CCITT_TABLE[256] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, + 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, + 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, + 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, + 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, + 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, + 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, + 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, + 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, + 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, + 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0}; + + uint16 crc = initValue; + + for (uint32 i = 0; i < size; i++) + { + const uint16 tmp = (crc >> 8) ^ buffer[i]; + crc = (crc << 8) ^ CRC_CCITT_TABLE[tmp]; + } + + return crc; + } + SkylanderPortalDevice::SkylanderPortalDevice() + : Device(0x1430, 0x0150, 1, 2, 0) + { + m_IsOpened = false; + } + + bool SkylanderPortalDevice::Open() + { + if (!IsOpened()) + { + m_IsOpened = true; + } + return true; + } + + void SkylanderPortalDevice::Close() + { + if (IsOpened()) + { + m_IsOpened = false; + } + } + + bool SkylanderPortalDevice::IsOpened() + { + return m_IsOpened; + } + + Device::ReadResult SkylanderPortalDevice::Read(ReadMessage* message) + { + memcpy(message->data, g_skyportal.GetStatus().data(), message->length); + message->bytesRead = message->length; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + return Device::ReadResult::Success; + } + + Device::WriteResult SkylanderPortalDevice::Write(WriteMessage* message) + { + message->bytesWritten = message->length; + return Device::WriteResult::Success; + } + + bool SkylanderPortalDevice::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 + 2) = 0x02; // bEndpointAddress + *(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 4) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 6) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + + cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29); + + memcpy(output, configurationDescriptor, + std::min(outputMaxLength, sizeof(configurationDescriptor))); + return true; + } + + bool SkylanderPortalDevice::SetProtocol(uint32 ifIndex, uint32 protocol) + { + return true; + } + + bool SkylanderPortalDevice::SetReport(ReportMessage* message) + { + g_skyportal.ControlTransfer(message->originalData, message->originalLength); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return true; + } + + void SkylanderUSB::ControlTransfer(uint8* buf, sint32 originalLength) + { + std::array interruptResponse = {}; + switch (buf[0]) + { + case 'A': + { + interruptResponse = {buf[0], buf[1], 0xFF, 0x77}; + g_skyportal.Activate(); + break; + } + case 'C': + { + g_skyportal.SetLeds(0x01, buf[1], buf[2], buf[3]); + break; + } + case 'J': + { + g_skyportal.SetLeds(buf[1], buf[2], buf[3], buf[4]); + interruptResponse = {buf[0]}; + break; + } + case 'L': + { + uint8 side = buf[1]; + if (side == 0x02) + { + side = 0x04; + } + g_skyportal.SetLeds(side, buf[2], buf[3], buf[4]); + break; + } + case 'M': + { + interruptResponse = {buf[0], buf[1], 0x00, 0x19}; + break; + } + case 'Q': + { + const uint8 skyNum = buf[1] & 0xF; + const uint8 block = buf[2]; + g_skyportal.QueryBlock(skyNum, block, interruptResponse.data()); + break; + } + case 'R': + { + interruptResponse = {buf[0], 0x02, 0x1b}; + break; + } + case 'S': + case 'V': + { + // No response needed + break; + } + case 'W': + { + const uint8 skyNum = buf[1] & 0xF; + const uint8 block = buf[2]; + g_skyportal.WriteBlock(skyNum, block, &buf[3], interruptResponse.data()); + break; + } + default: + cemu_assert_error(); + break; + } + if (interruptResponse[0] != 0) + { + std::lock_guard lock(m_queryMutex); + m_queries.push(interruptResponse); + } + } + + void SkylanderUSB::Activate() + { + std::lock_guard lock(m_skyMutex); + if (m_activated) + { + // If the portal was already active no change is needed + return; + } + + // If not we need to advertise change to all the figures present on the portal + for (auto& s : m_skylanders) + { + if (s.status & 1) + { + s.queuedStatus.push(3); + s.queuedStatus.push(1); + } + } + + m_activated = true; + } + + void SkylanderUSB::Deactivate() + { + std::lock_guard lock(m_skyMutex); + + for (auto& s : m_skylanders) + { + // check if at the end of the updates there would be a figure on the portal + if (!s.queuedStatus.empty()) + { + s.status = s.queuedStatus.back(); + s.queuedStatus = std::queue(); + } + + s.status &= 1; + } + + m_activated = false; + } + + void SkylanderUSB::SetLeds(uint8 side, uint8 r, uint8 g, uint8 b) + { + std::lock_guard lock(m_skyMutex); + if (side == 0x00) + { + m_colorRight.red = r; + m_colorRight.green = g; + m_colorRight.blue = b; + } + else if (side == 0x01) + { + m_colorRight.red = r; + m_colorRight.green = g; + m_colorRight.blue = b; + + m_colorLeft.red = r; + m_colorLeft.green = g; + m_colorLeft.blue = b; + } + else if (side == 0x02) + { + m_colorLeft.red = r; + m_colorLeft.green = g; + m_colorLeft.blue = b; + } + else if (side == 0x03) + { + m_colorTrap.red = r; + m_colorTrap.green = g; + m_colorTrap.blue = b; + } + } + + uint8 SkylanderUSB::LoadSkylander(uint8* buf, std::unique_ptr file) + { + std::lock_guard lock(m_skyMutex); + + uint32 skySerial = 0; + for (int i = 3; i > -1; i--) + { + skySerial <<= 8; + skySerial |= buf[i]; + } + uint8 foundSlot = 0xFF; + + // mimics spot retaining on the portal + for (auto i = 0; i < 16; i++) + { + if ((m_skylanders[i].status & 1) == 0) + { + if (m_skylanders[i].lastId == skySerial) + { + foundSlot = i; + break; + } + + if (i < foundSlot) + { + foundSlot = i; + } + } + } + + if (foundSlot != 0xFF) + { + auto& skylander = m_skylanders[foundSlot]; + memcpy(skylander.data.data(), buf, skylander.data.size()); + skylander.skyFile = std::move(file); + skylander.status = Skylander::ADDED; + skylander.queuedStatus.push(Skylander::ADDED); + skylander.queuedStatus.push(Skylander::READY); + skylander.lastId = skySerial; + } + return foundSlot; + } + + bool SkylanderUSB::RemoveSkylander(uint8 skyNum) + { + std::lock_guard lock(m_skyMutex); + auto& thesky = m_skylanders[skyNum]; + + if (thesky.status & 1) + { + thesky.status = 2; + thesky.queuedStatus.push(2); + thesky.queuedStatus.push(0); + thesky.Save(); + thesky.skyFile.reset(); + return true; + } + + return false; + } + + void SkylanderUSB::QueryBlock(uint8 skyNum, uint8 block, uint8* replyBuf) + { + std::lock_guard lock(m_skyMutex); + + const auto& skylander = m_skylanders[skyNum]; + + replyBuf[0] = 'Q'; + replyBuf[2] = block; + if (skylander.status & 1) + { + replyBuf[1] = (0x10 | skyNum); + memcpy(replyBuf + 3, skylander.data.data() + (16 * block), 16); + } + else + { + replyBuf[1] = skyNum; + } + } + + void SkylanderUSB::WriteBlock(uint8 skyNum, uint8 block, + const uint8* toWriteBuf, uint8* replyBuf) + { + std::lock_guard lock(m_skyMutex); + + auto& skylander = m_skylanders[skyNum]; + + replyBuf[0] = 'W'; + replyBuf[2] = block; + + if (skylander.status & 1) + { + replyBuf[1] = (0x10 | skyNum); + memcpy(skylander.data.data() + (block * 16), toWriteBuf, 16); + skylander.Save(); + } + else + { + replyBuf[1] = skyNum; + } + } + + std::array SkylanderUSB::GetStatus() + { + std::lock_guard lock(m_queryMutex); + std::array interruptResponse = {}; + + if (!m_queries.empty()) + { + interruptResponse = m_queries.front(); + m_queries.pop(); + // This needs to happen after ~22 milliseconds + } + else + { + uint32 status = 0; + uint8 active = 0x00; + if (m_activated) + { + active = 0x01; + } + + for (int i = 16 - 1; i >= 0; i--) + { + auto& s = m_skylanders[i]; + + if (!s.queuedStatus.empty()) + { + s.status = s.queuedStatus.front(); + s.queuedStatus.pop(); + } + status <<= 2; + status |= s.status; + } + interruptResponse = {0x53, 0x00, 0x00, 0x00, 0x00, m_interruptCounter++, + active, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; + memcpy(&interruptResponse[1], &status, sizeof(status)); + } + return interruptResponse; + } + + void SkylanderUSB::Skylander::Save() + { + if (!skyFile) + return; + + skyFile->writeData(data.data(), data.size()); + } +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h new file mode 100644 index 00000000..dfa370dc --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -0,0 +1,98 @@ +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +namespace nsyshid +{ + class SkylanderPortalDevice final : public Device { + public: + SkylanderPortalDevice(); + ~SkylanderPortalDevice() = 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(uint32 ifIndex, uint32 protocol) override; + + bool SetReport(ReportMessage* message) override; + + private: + bool m_IsOpened; + }; + + extern const std::map, const std::string> listSkylanders; + + class SkylanderUSB { + public: + struct Skylander final + { + std::unique_ptr skyFile; + uint8 status = 0; + std::queue queuedStatus; + std::array data{}; + uint32 lastId = 0; + void Save(); + + enum : uint8 + { + REMOVED = 0, + READY = 1, + REMOVING = 2, + ADDED = 3 + }; + }; + + struct SkylanderLEDColor final + { + uint8 red = 0; + uint8 green = 0; + uint8 blue = 0; + }; + + void ControlTransfer(uint8* buf, sint32 originalLength); + + void Activate(); + void Deactivate(); + void SetLeds(uint8 side, uint8 r, uint8 g, uint8 b); + + std::array GetStatus(); + void QueryBlock(uint8 skyNum, uint8 block, uint8* replyBuf); + void WriteBlock(uint8 skyNum, uint8 block, const uint8* toWriteBuf, + uint8* replyBuf); + + uint8 LoadSkylander(uint8* buf, std::unique_ptr file); + bool RemoveSkylander(uint8 skyNum); + uint16 SkylanderCRC16(uint16 initValue, const uint8* buffer, uint32 size); + + protected: + std::mutex m_skyMutex; + std::mutex m_queryMutex; + std::array m_skylanders; + + private: + std::queue> m_queries; + bool m_activated = true; + uint8 m_interruptCounter = 0; + SkylanderLEDColor m_colorRight = {}; + SkylanderLEDColor m_colorLeft = {}; + SkylanderLEDColor m_colorTrap = {}; + + }; + extern SkylanderUSB g_skyportal; +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/nsyshid.cpp b/src/Cafe/OS/libs/nsyshid/nsyshid.cpp index ff5c4f45..c674b844 100644 --- a/src/Cafe/OS/libs/nsyshid/nsyshid.cpp +++ b/src/Cafe/OS/libs/nsyshid/nsyshid.cpp @@ -256,6 +256,19 @@ namespace nsyshid device->m_productId); } + bool FindDeviceById(uint16 vendorId, uint16 productId) + { + std::lock_guard lock(hidMutex); + for (const auto& device : deviceList) + { + if (device->m_vendorId == vendorId && device->m_productId == productId) + { + return true; + } + } + return false; + } + void export_HIDAddClient(PPCInterpreter_t* hCPU) { ppcDefineParamTypePtr(hidClient, HIDClient_t, 0); @@ -406,7 +419,8 @@ namespace nsyshid sint32 originalLength, MPTR callbackFuncMPTR, MPTR callbackParamMPTR) { cemuLog_logDebug(LogType::Force, "_hidSetReportAsync begin"); - if (device->SetReport(reportData, length, originalData, originalLength)) + ReportMessage message(reportData, length, originalData, originalLength); + if (device->SetReport(&message)) { DoHIDTransferCallback(callbackFuncMPTR, callbackParamMPTR, @@ -433,7 +447,8 @@ namespace nsyshid { _debugPrintHex("_hidSetReportSync Begin", reportData, length); sint32 returnCode = 0; - if (device->SetReport(reportData, length, originalData, originalLength)) + ReportMessage message(reportData, length, originalData, originalLength); + if (device->SetReport(&message)) { returnCode = originalLength; } @@ -511,17 +526,16 @@ namespace nsyshid return -1; } memset(data, 0, maxLength); - - sint32 bytesRead = 0; - Device::ReadResult readResult = device->Read(data, maxLength, bytesRead); + ReadMessage message(data, maxLength, 0); + Device::ReadResult readResult = device->Read(&message); switch (readResult) { case Device::ReadResult::Success: { cemuLog_logDebug(LogType::Force, "nsyshid.hidReadInternalSync(): read {} of {} bytes", - bytesRead, + message.bytesRead, maxLength); - return bytesRead; + return message.bytesRead; } break; case Device::ReadResult::Error: @@ -609,15 +623,15 @@ namespace nsyshid cemuLog_logDebug(LogType::Force, "nsyshid.hidWriteInternalSync(): cannot write to a non-opened device"); return -1; } - sint32 bytesWritten = 0; - Device::WriteResult writeResult = device->Write(data, maxLength, bytesWritten); + WriteMessage message(data, maxLength, 0); + Device::WriteResult writeResult = device->Write(&message); switch (writeResult) { case Device::WriteResult::Success: { - cemuLog_logDebug(LogType::Force, "nsyshid.hidWriteInternalSync(): wrote {} of {} bytes", bytesWritten, + cemuLog_logDebug(LogType::Force, "nsyshid.hidWriteInternalSync(): wrote {} of {} bytes", message.bytesWritten, maxLength); - return bytesWritten; + return message.bytesWritten; } break; case Device::WriteResult::Error: @@ -758,6 +772,11 @@ namespace nsyshid return nullptr; } + bool Backend::FindDeviceById(uint16 vendorId, uint16 productId) + { + return nsyshid::FindDeviceById(vendorId, productId); + } + bool Backend::IsDeviceWhitelisted(uint16 vendorId, uint16 productId) { return Whitelist::GetInstance().IsDeviceWhitelisted(vendorId, productId); diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 4f1736e2..8e7cf398 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -358,6 +358,10 @@ void CemuConfig::Load(XMLConfigParser& parser) auto dsuc = input.get("DSUC"); dsu_client.host = dsuc.get_attribute("host", dsu_client.host); dsu_client.port = dsuc.get_attribute("port", dsu_client.port); + + // emulatedusbdevices + auto usbdevices = parser.get("EmulatedUsbDevices"); + emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); } void CemuConfig::Save(XMLConfigParser& parser) @@ -551,6 +555,10 @@ void CemuConfig::Save(XMLConfigParser& parser) auto dsuc = input.set("DSUC"); dsuc.set_attribute("host", dsu_client.host); dsuc.set_attribute("port", dsu_client.port); + + // emulated usb devices + auto usbdevices = config.set("EmulatedUsbDevices"); + usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); } GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index cab7a1af..d0776d2e 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -514,6 +514,12 @@ struct CemuConfig NetworkService GetAccountNetworkService(uint32 persistentId); void SetAccountSelectedService(uint32 persistentId, NetworkService serviceIndex); + + // emulated usb devices + struct + { + ConfigValue emulate_skylander_portal{false}; + }emulated_usb_devices{}; private: GameEntry* GetGameEntryByTitleId(uint64 titleId); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 19ce95dc..02f96a9c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -101,6 +101,8 @@ add_library(CemuGui PairingDialog.h TitleManager.cpp TitleManager.h + EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp + EmulatedUSBDevices/EmulatedUSBDeviceFrame.h windows/PPCThreadsViewer windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp windows/PPCThreadsViewer/DebugPPCThreadsWindow.h diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp new file mode 100644 index 00000000..58c1823c --- /dev/null +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -0,0 +1,354 @@ +#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h" + +#include +#include + +#include "config/CemuConfig.h" +#include "gui/helpers/wxHelpers.h" +#include "gui/wxHelper.h" +#include "util/helpers/helpers.h" + +#include "Cafe/OS/libs/nsyshid/nsyshid.h" +#include "Cafe/OS/libs/nsyshid/Skylander.h" + +#include "Common/FileStream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "resource/embedded/resources.h" +#include "EmulatedUSBDeviceFrame.h" + +EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) + : wxFrame(parent, wxID_ANY, _("Emulated USB Devices"), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) +{ + SetIcon(wxICON(X_BOX)); + + auto& config = GetConfig(); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + auto* notebook = new wxNotebook(this, wxID_ANY); + + notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal")); + + sizer->Add(notebook, 1, wxEXPAND | wxALL, 2); + + SetSizerAndFit(sizer); + Layout(); + Centre(wxBOTH); +} + +EmulatedUSBDeviceFrame::~EmulatedUSBDeviceFrame() {} + +wxPanel* EmulatedUSBDeviceFrame::AddSkylanderPage(wxNotebook* notebook) +{ + auto* panel = new wxPanel(notebook); + auto* panelSizer = new wxBoxSizer(wxVERTICAL); + auto* box = new wxStaticBox(panel, wxID_ANY, _("Skylanders Manager")); + auto* boxSizer = new wxStaticBoxSizer(box, wxVERTICAL); + + auto* row = new wxBoxSizer(wxHORIZONTAL); + + m_emulatePortal = + new wxCheckBox(box, wxID_ANY, _("Emulate Skylander Portal")); + m_emulatePortal->SetValue( + GetConfig().emulated_usb_devices.emulate_skylander_portal); + m_emulatePortal->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + GetConfig().emulated_usb_devices.emulate_skylander_portal = + m_emulatePortal->IsChecked(); + g_config.Save(); + }); + row->Add(m_emulatePortal, 1, wxEXPAND | wxALL, 2); + boxSizer->Add(row, 1, wxEXPAND | wxALL, 2); + for (int i = 0; i < 16; i++) + { + boxSizer->Add(AddSkylanderRow(i, box), 1, wxEXPAND | wxALL, 2); + } + panelSizer->Add(boxSizer, 1, wxEXPAND | wxALL, 2); + panel->SetSizerAndFit(panelSizer); + + return panel; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 row_number, + wxStaticBox* box) +{ + auto* row = new wxBoxSizer(wxHORIZONTAL); + + row->Add(new wxStaticText(box, wxID_ANY, + fmt::format("{} {}", _("Skylander").ToStdString(), + (row_number + 1))), + 1, wxEXPAND | wxALL, 2); + m_skylanderSlots[row_number] = + new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, + wxTE_READONLY); + m_skylanderSlots[row_number]->SetMinSize(wxSize(150, -1)); + m_skylanderSlots[row_number]->Disable(); + row->Add(m_skylanderSlots[row_number], 1, wxEXPAND | wxALL, 2); + auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); + loadButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { + LoadSkylander(row_number); + }); + auto* createButton = new wxButton(box, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { + CreateSkylander(row_number); + }); + auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); + clearButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { + ClearSkylander(row_number); + }); + row->Add(loadButton, 1, wxEXPAND | wxALL, 2); + row->Add(createButton, 1, wxEXPAND | wxALL, 2); + row->Add(clearButton, 1, wxEXPAND | wxALL, 2); + + return row; +} + +void EmulatedUSBDeviceFrame::LoadSkylander(uint8 slot) +{ + wxFileDialog openFileDialog(this, _("Open Skylander dump"), "", "", + "Skylander files (*.sky;*.bin;*.dump;*.dmp)|*.sky;*.bin;*.dump;*.dmp", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) + return; + + LoadSkylanderPath(slot, openFileDialog.GetPath()); +} + +void EmulatedUSBDeviceFrame::LoadSkylanderPath(uint8 slot, wxString path) +{ + std::unique_ptr skyFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); + if (!skyFile) + { + wxMessageDialog open_error(this, "Error Opening File: " + path.c_str()); + open_error.ShowModal(); + return; + } + + std::array fileData; + if (skyFile->readData(fileData.data(), fileData.size()) != fileData.size()) + { + wxMessageDialog open_error(this, "Failed to read file! File was too small"); + open_error.ShowModal(); + return; + } + ClearSkylander(slot); + + uint16 skyId = uint16(fileData[0x11]) << 8 | uint16(fileData[0x10]); + uint16 skyVar = uint16(fileData[0x1D]) << 8 | uint16(fileData[0x1C]); + + uint8 portalSlot = nsyshid::g_skyportal.LoadSkylander(fileData.data(), + std::move(skyFile)); + m_skySlots[slot] = std::tuple(portalSlot, skyId, skyVar); + UpdateSkylanderEdits(); +} + +void EmulatedUSBDeviceFrame::CreateSkylander(uint8 slot) +{ + CreateSkylanderDialog create_dlg(this, slot); + create_dlg.ShowModal(); + if (create_dlg.GetReturnCode() == 1) + { + LoadSkylanderPath(slot, create_dlg.GetFilePath()); + } +} + +void EmulatedUSBDeviceFrame::ClearSkylander(uint8 slot) +{ + if (auto slotInfos = m_skySlots[slot]) + { + auto [curSlot, id, var] = slotInfos.value(); + nsyshid::g_skyportal.RemoveSkylander(curSlot); + m_skySlots[slot] = {}; + UpdateSkylanderEdits(); + } +} + +CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) + : wxDialog(parent, wxID_ANY, _("Skylander Figure Creator"), wxDefaultPosition, wxSize(500, 150)) +{ + 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 (auto it = nsyshid::listSkylanders.begin(); it != nsyshid::listSkylanders.end(); it++) + { + const uint32 variant = uint32(uint32(it->first.first) << 16) | uint32(it->first.second); + comboBox->Append(it->second, reinterpret_cast(variant)); + filterlist.Add(it->second); + } + comboBox->SetSelection(0); + bool enabled = comboBox->AutoComplete(filterlist); + comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); + + auto* idVarRow = new wxBoxSizer(wxHORIZONTAL); + + wxIntegerValidator validator; + + auto* labelId = new wxStaticText(this, wxID_ANY, "ID:"); + auto* labelVar = new wxStaticText(this, wxID_ANY, "Variant:"); + auto* editId = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); + auto* editVar = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); + + idVarRow->Add(labelId, 1, wxALL, 5); + idVarRow->Add(editId, 1, wxALL, 5); + idVarRow->Add(labelVar, 1, wxALL, 5); + idVarRow->Add(editVar, 1, wxALL, 5); + + auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); + + auto* createButton = new wxButton(this, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [editId, editVar, this](wxCommandEvent&) { + long longSkyId; + if (!editId->GetValue().ToLong(&longSkyId) || longSkyId > 0xFFFF) + { + wxMessageDialog id_error(this, "Error Converting ID!", "ID Entered is Invalid"); + id_error.ShowModal(); + return; + } + long longSkyVar; + if (!editVar->GetValue().ToLong(&longSkyVar) || longSkyVar > 0xFFFF) + { + wxMessageDialog id_error(this, "Error Converting Variant!", "Variant Entered is Invalid"); + id_error.ShowModal(); + return; + } + uint16 skyId = longSkyId & 0xFFFF; + uint16 skyVar = longSkyVar & 0xFFFF; + const auto foundSky = nsyshid::listSkylanders.find(std::make_pair(skyId, skyVar)); + wxString predefName; + if (foundSky != nsyshid::listSkylanders.end()) + { + predefName = foundSky->second + ".sky"; + } + else + { + predefName = wxString::Format(_("Unknown(%i %i).sky"), skyId, skyVar); + } + wxFileDialog + saveFileDialog(this, _("Create Skylander file"), "", predefName, + "SKY files (*.sky)|*.sky", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (saveFileDialog.ShowModal() == wxID_CANCEL) + return; + + m_filePath = saveFileDialog.GetPath(); + + wxFileOutputStream output_stream(saveFileDialog.GetPath()); + if (!output_stream.IsOk()) + { + wxMessageDialog saveError(this, "Error Creating Skylander File"); + return; + } + + std::array data{}; + + uint32 first_block = 0x690F0F0F; + uint32 other_blocks = 0x69080F7F; + memcpy(&data[0x36], &first_block, sizeof(first_block)); + for (size_t index = 1; index < 0x10; index++) + { + memcpy(&data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); + } + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0, 255); + data[0] = dist(mt); + data[1] = dist(mt); + data[2] = dist(mt); + data[3] = dist(mt); + data[4] = data[0] ^ data[1] ^ data[2] ^ data[3]; + data[5] = 0x81; + data[6] = 0x01; + data[7] = 0x0F; + + memcpy(&data[0x10], &skyId, sizeof(skyId)); + memcpy(&data[0x1C], &skyVar, sizeof(skyVar)); + + uint16 crc = nsyshid::g_skyportal.SkylanderCRC16(0xFFFF, data.data(), 0x1E); + + memcpy(&data[0x1E], &crc, sizeof(crc)); + + output_stream.SeekO(0); + output_stream.WriteAll(data.data(), data.size()); + output_stream.Close(); + + 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, editId, editVar, this](wxCommandEvent&) { + const uint64 sky_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); + if (sky_info != 0xFFFFFFFF) + { + const uint16 skyId = sky_info >> 16; + const uint16 skyVar = sky_info & 0xFFFF; + + editId->SetValue(wxString::Format(wxT("%i"), skyId)); + editVar->SetValue(wxString::Format(wxT("%i"), skyVar)); + } + }); + + buttonRow->Add(createButton, 1, wxALL, 5); + buttonRow->Add(cancelButton, 1, wxALL, 5); + + sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(idVarRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); + + this->SetSizer(sizer); + this->Centre(wxBOTH); +} + +wxString CreateSkylanderDialog::GetFilePath() const +{ + return m_filePath; +} + +void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() +{ + for (auto i = 0; i < 16; i++) + { + std::string displayString; + if (auto sd = m_skySlots[i]) + { + auto [portalSlot, skyId, skyVar] = sd.value(); + auto foundSky = nsyshid::listSkylanders.find(std::make_pair(skyId, skyVar)); + if (foundSky != nsyshid::listSkylanders.end()) + { + displayString = foundSky->second; + } + else + { + displayString = fmt::format("Unknown (Id:{} Var:{})", skyId, skyVar); + } + } + else + { + displayString = "None"; + } + + m_skylanderSlots[i]->ChangeValue(displayString); + } +} \ No newline at end of file diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h new file mode 100644 index 00000000..6acb7da8 --- /dev/null +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include + +class wxBoxSizer; +class wxCheckBox; +class wxFlexGridSizer; +class wxNotebook; +class wxPanel; +class wxStaticBox; +class wxString; +class wxTextCtrl; + +class EmulatedUSBDeviceFrame : public wxFrame { + public: + EmulatedUSBDeviceFrame(wxWindow* parent); + ~EmulatedUSBDeviceFrame(); + + private: + wxCheckBox* m_emulatePortal; + std::array m_skylanderSlots; + std::array>, 16> m_skySlots; + + wxPanel* AddSkylanderPage(wxNotebook* notebook); + wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); + void LoadSkylander(uint8 slot); + void LoadSkylanderPath(uint8 slot, wxString path); + void CreateSkylander(uint8 slot); + void ClearSkylander(uint8 slot); + void UpdateSkylanderEdits(); +}; +class CreateSkylanderDialog : public wxDialog { + public: + explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot); + wxString GetFilePath() const; + + protected: + wxString m_filePath; +}; \ No newline at end of file diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 03c69a7f..7a4f3174 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -30,6 +30,7 @@ #include "Cafe/Filesystem/FST/FST.h" #include "gui/TitleManager.h" +#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h" #include "Cafe/CafeSystem.h" @@ -110,6 +111,7 @@ enum MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER = 20600, MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, + MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, // cpu // cpu->timer speed MAINFRAME_MENU_ID_TIMER_SPEED_1X = 20700, @@ -188,6 +190,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, MainWindow::OnToolsInput) +EVT_MENU(MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, MainWindow::OnToolsInput) // cpu menu EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_8X, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_4X, MainWindow::OnDebugSetting) @@ -1515,6 +1518,29 @@ void MainWindow::OnToolsInput(wxCommandEvent& event) }); m_title_manager->Show(); } + break; + } + case MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES: + { + if (m_usb_devices) + { + m_usb_devices->Show(true); + m_usb_devices->Raise(); + m_usb_devices->SetFocus(); + } + else + { + m_usb_devices = new EmulatedUSBDeviceFrame(this); + m_usb_devices->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) + { + if (event.CanVeto()) { + m_usb_devices->Show(false); + event.Veto(); + } + }); + m_usb_devices->Show(true); + } + break; } break; } @@ -2166,6 +2192,7 @@ void MainWindow::RecreateMenu() m_memorySearcherMenuItem->Enable(false); toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, _("&Title Manager")); toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, _("&Download Manager")); + toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, _("&Emulated USB Devices")); m_menuBar->Append(toolsMenu, _("&Tools")); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 7191df12..dd4d0d0d 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -22,6 +22,7 @@ struct GameEntry; class DiscordPresence; class TitleManager; class GraphicPacksWindow2; +class EmulatedUSBDeviceFrame; class wxLaunchGameEvent; wxDECLARE_EVENT(wxEVT_LAUNCH_GAME, wxLaunchGameEvent); @@ -164,6 +165,7 @@ private: MemorySearcherTool* m_toolWindow = nullptr; TitleManager* m_title_manager = nullptr; + EmulatedUSBDeviceFrame* m_usb_devices = nullptr; PadViewFrame* m_padView = nullptr; GraphicPacksWindow2* m_graphic_pack_window = nullptr; From aefbb918beb8718af8f190a73018ff63bf801d95 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Fri, 28 Jun 2024 14:44:49 +0100 Subject: [PATCH 27/73] nsyshid: Skylander emulation fixes and code cleanup (#1244) --- src/Cafe/OS/libs/nsyshid/Skylander.cpp | 79 +++++++++++++++++-- src/Cafe/OS/libs/nsyshid/Skylander.h | 17 ++-- .../EmulatedUSBDeviceFrame.cpp | 78 ++++-------------- .../EmulatedUSBDeviceFrame.h | 6 +- 4 files changed, 101 insertions(+), 79 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp index 3123d14d..241e1969 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.cpp +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -1,5 +1,7 @@ #include "Skylander.h" +#include + #include "nsyshid.h" #include "Backend.h" @@ -9,8 +11,8 @@ namespace nsyshid { SkylanderUSB g_skyportal; - const std::map, const std::string> - listSkylanders = { + const std::map, const char*> + s_listSkylanders = { {{0, 0x0000}, "Whirlwind"}, {{0, 0x1801}, "Series 2 Whirlwind"}, {{0, 0x1C02}, "Polar Whirlwind"}, @@ -845,6 +847,49 @@ namespace nsyshid return false; } + bool SkylanderUSB::CreateSkylander(fs::path pathName, uint16 skyId, uint16 skyVar) + { + FileStream* skyFile(FileStream::createFile2(pathName)); + if (!skyFile) + { + return false; + } + + std::array data{}; + + uint32 first_block = 0x690F0F0F; + uint32 other_blocks = 0x69080F7F; + memcpy(&data[0x36], &first_block, sizeof(first_block)); + for (size_t index = 1; index < 0x10; index++) + { + memcpy(&data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); + } + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0, 255); + data[0] = dist(mt); + data[1] = dist(mt); + data[2] = dist(mt); + data[3] = dist(mt); + data[4] = data[0] ^ data[1] ^ data[2] ^ data[3]; + data[5] = 0x81; + data[6] = 0x01; + data[7] = 0x0F; + + memcpy(&data[0x10], &skyId, sizeof(skyId)); + memcpy(&data[0x1C], &skyVar, sizeof(skyVar)); + + uint16 crc = nsyshid::g_skyportal.SkylanderCRC16(0xFFFF, data.data(), 0x1E); + + memcpy(&data[0x1E], &crc, sizeof(crc)); + + skyFile->writeData(data.data(), data.size()); + + delete skyFile; + + return true; + } + void SkylanderUSB::QueryBlock(uint8 skyNum, uint8 block, uint8* replyBuf) { std::lock_guard lock(m_skyMutex); @@ -865,7 +910,7 @@ namespace nsyshid } void SkylanderUSB::WriteBlock(uint8 skyNum, uint8 block, - const uint8* toWriteBuf, uint8* replyBuf) + const uint8* toWriteBuf, uint8* replyBuf) { std::lock_guard lock(m_skyMutex); @@ -919,21 +964,39 @@ namespace nsyshid status |= s.status; } interruptResponse = {0x53, 0x00, 0x00, 0x00, 0x00, m_interruptCounter++, - active, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00}; + active, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; memcpy(&interruptResponse[1], &status, sizeof(status)); } return interruptResponse; } + std::string SkylanderUSB::FindSkylander(uint16 skyId, uint16 skyVar) + { + for (const auto& it : GetListSkylanders()) + { + if(it.first.first == skyId && it.first.second == skyVar) + { + return it.second; + } + } + return fmt::format("Unknown ({} {})", skyId, skyVar); + } + + std::map, const char*> SkylanderUSB::GetListSkylanders() + { + return s_listSkylanders; + } + void SkylanderUSB::Skylander::Save() { if (!skyFile) return; + skyFile->SetPosition(0); skyFile->writeData(data.data(), data.size()); } } // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h index dfa370dc..a1ca7f8f 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.h +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -1,3 +1,5 @@ +#pragma once + #include #include "nsyshid.h" @@ -36,7 +38,10 @@ namespace nsyshid bool m_IsOpened; }; - extern const std::map, const std::string> listSkylanders; + constexpr uint16 BLOCK_COUNT = 0x40; + constexpr uint16 BLOCK_SIZE = 0x10; + constexpr uint16 FIGURE_SIZE = BLOCK_COUNT * BLOCK_SIZE; + constexpr uint8 MAX_SKYLANDERS = 16; class SkylanderUSB { public: @@ -45,7 +50,7 @@ namespace nsyshid std::unique_ptr skyFile; uint8 status = 0; std::queue queuedStatus; - std::array data{}; + std::array data{}; uint32 lastId = 0; void Save(); @@ -74,16 +79,19 @@ namespace nsyshid std::array GetStatus(); void QueryBlock(uint8 skyNum, uint8 block, uint8* replyBuf); void WriteBlock(uint8 skyNum, uint8 block, const uint8* toWriteBuf, - uint8* replyBuf); + uint8* replyBuf); uint8 LoadSkylander(uint8* buf, std::unique_ptr file); bool RemoveSkylander(uint8 skyNum); + bool CreateSkylander(fs::path pathName, uint16 skyId, uint16 skyVar); uint16 SkylanderCRC16(uint16 initValue, const uint8* buffer, uint32 size); + static std::map, const char*> GetListSkylanders(); + std::string FindSkylander(uint16 skyId, uint16 skyVar); protected: std::mutex m_skyMutex; std::mutex m_queryMutex; - std::array m_skylanders; + std::array m_skylanders; private: std::queue> m_queries; @@ -92,7 +100,6 @@ namespace nsyshid SkylanderLEDColor m_colorRight = {}; SkylanderLEDColor m_colorLeft = {}; SkylanderLEDColor m_colorTrap = {}; - }; extern SkylanderUSB g_skyportal; } // namespace nsyshid \ No newline at end of file diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index 58c1823c..f43c3690 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -1,7 +1,6 @@ #include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h" #include -#include #include "config/CemuConfig.h" #include "gui/helpers/wxHelpers.h" @@ -9,7 +8,6 @@ #include "util/helpers/helpers.h" #include "Cafe/OS/libs/nsyshid/nsyshid.h" -#include "Cafe/OS/libs/nsyshid/Skylander.h" #include "Common/FileStream.h" @@ -75,7 +73,7 @@ wxPanel* EmulatedUSBDeviceFrame::AddSkylanderPage(wxNotebook* notebook) }); row->Add(m_emulatePortal, 1, wxEXPAND | wxALL, 2); boxSizer->Add(row, 1, wxEXPAND | wxALL, 2); - for (int i = 0; i < 16; i++) + for (int i = 0; i < nsyshid::MAX_SKYLANDERS; i++) { boxSizer->Add(AddSkylanderRow(i, box), 1, wxEXPAND | wxALL, 2); } @@ -153,7 +151,7 @@ void EmulatedUSBDeviceFrame::LoadSkylanderPath(uint8 slot, wxString path) uint16 skyVar = uint16(fileData[0x1D]) << 8 | uint16(fileData[0x1C]); uint8 portalSlot = nsyshid::g_skyportal.LoadSkylander(fileData.data(), - std::move(skyFile)); + std::move(skyFile)); m_skySlots[slot] = std::tuple(portalSlot, skyId, skyVar); UpdateSkylanderEdits(); } @@ -189,11 +187,11 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) auto* comboBox = new wxComboBox(this, wxID_ANY); comboBox->Append("---Select---", reinterpret_cast(0xFFFFFFFF)); wxArrayString filterlist; - for (auto it = nsyshid::listSkylanders.begin(); it != nsyshid::listSkylanders.end(); it++) + for (const auto& it : nsyshid::g_skyportal.GetListSkylanders()) { - const uint32 variant = uint32(uint32(it->first.first) << 16) | uint32(it->first.second); - comboBox->Append(it->second, reinterpret_cast(variant)); - filterlist.Add(it->second); + const uint32 variant = uint32(uint32(it.first.first) << 16) | uint32(it.first.second); + comboBox->Append(it.second, reinterpret_cast(variant)); + filterlist.Add(it.second); } comboBox->SetSelection(0); bool enabled = comboBox->AutoComplete(filterlist); @@ -233,16 +231,7 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) } uint16 skyId = longSkyId & 0xFFFF; uint16 skyVar = longSkyVar & 0xFFFF; - const auto foundSky = nsyshid::listSkylanders.find(std::make_pair(skyId, skyVar)); - wxString predefName; - if (foundSky != nsyshid::listSkylanders.end()) - { - predefName = foundSky->second + ".sky"; - } - else - { - predefName = wxString::Format(_("Unknown(%i %i).sky"), skyId, skyVar); - } + wxString predefName = nsyshid::g_skyportal.FindSkylander(skyId, skyVar) + ".sky"; wxFileDialog saveFileDialog(this, _("Create Skylander file"), "", predefName, "SKY files (*.sky)|*.sky", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); @@ -251,46 +240,15 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) return; m_filePath = saveFileDialog.GetPath(); - - wxFileOutputStream output_stream(saveFileDialog.GetPath()); - if (!output_stream.IsOk()) + + if(!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar)) { - wxMessageDialog saveError(this, "Error Creating Skylander File"); + wxMessageDialog errorMessage(this, "Failed to create file"); + errorMessage.ShowModal(); + this->EndModal(0); return; } - std::array data{}; - - uint32 first_block = 0x690F0F0F; - uint32 other_blocks = 0x69080F7F; - memcpy(&data[0x36], &first_block, sizeof(first_block)); - for (size_t index = 1; index < 0x10; index++) - { - memcpy(&data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); - } - std::random_device rd; - std::mt19937 mt(rd()); - std::uniform_int_distribution dist(0, 255); - data[0] = dist(mt); - data[1] = dist(mt); - data[2] = dist(mt); - data[3] = dist(mt); - data[4] = data[0] ^ data[1] ^ data[2] ^ data[3]; - data[5] = 0x81; - data[6] = 0x01; - data[7] = 0x0F; - - memcpy(&data[0x10], &skyId, sizeof(skyId)); - memcpy(&data[0x1C], &skyVar, sizeof(skyVar)); - - uint16 crc = nsyshid::g_skyportal.SkylanderCRC16(0xFFFF, data.data(), 0x1E); - - memcpy(&data[0x1E], &crc, sizeof(crc)); - - output_stream.SeekO(0); - output_stream.WriteAll(data.data(), data.size()); - output_stream.Close(); - this->EndModal(1); }); auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); @@ -328,21 +286,13 @@ wxString CreateSkylanderDialog::GetFilePath() const void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() { - for (auto i = 0; i < 16; i++) + for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) { std::string displayString; if (auto sd = m_skySlots[i]) { auto [portalSlot, skyId, skyVar] = sd.value(); - auto foundSky = nsyshid::listSkylanders.find(std::make_pair(skyId, skyVar)); - if (foundSky != nsyshid::listSkylanders.end()) - { - displayString = foundSky->second; - } - else - { - displayString = fmt::format("Unknown (Id:{} Var:{})", skyId, skyVar); - } + displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar); } else { diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h index 6acb7da8..8988cb8a 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h @@ -5,6 +5,8 @@ #include #include +#include "Cafe/OS/libs/nsyshid/Skylander.h" + class wxBoxSizer; class wxCheckBox; class wxFlexGridSizer; @@ -21,8 +23,8 @@ class EmulatedUSBDeviceFrame : public wxFrame { private: wxCheckBox* m_emulatePortal; - std::array m_skylanderSlots; - std::array>, 16> m_skySlots; + std::array m_skylanderSlots; + std::array>, nsyshid::MAX_SKYLANDERS> m_skySlots; wxPanel* AddSkylanderPage(wxNotebook* notebook); wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); From 64b0b85ed5fe15d17cfa6b5fce0044000b013a07 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Sat, 29 Jun 2024 21:31:47 +0100 Subject: [PATCH 28/73] Create GamePad window at correct size (#1247) Don't change the size on canvas initialization --- src/gui/PadViewFrame.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/gui/PadViewFrame.cpp b/src/gui/PadViewFrame.cpp index f2da2ca7..e7cc5c18 100644 --- a/src/gui/PadViewFrame.cpp +++ b/src/gui/PadViewFrame.cpp @@ -20,18 +20,24 @@ extern WindowInfo g_window_info; +#define PAD_MIN_WIDTH 320 +#define PAD_MIN_HEIGHT 180 + PadViewFrame::PadViewFrame(wxFrame* parent) - : wxFrame(nullptr, wxID_ANY, _("GamePad View"), wxDefaultPosition, wxSize(854, 480), wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxCLOSE_BOX | wxWANTS_CHARS) + : wxFrame(nullptr, wxID_ANY, _("GamePad View"), wxDefaultPosition, wxDefaultSize, wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxCLOSE_BOX | wxWANTS_CHARS) { gui_initHandleContextFromWxWidgetsWindow(g_window_info.window_pad, this); - + SetIcon(wxICON(M_WND_ICON128)); wxWindow::EnableTouchEvents(wxTOUCH_PAN_GESTURES); - SetMinClientSize({ 320, 180 }); + SetMinClientSize({ PAD_MIN_WIDTH, PAD_MIN_HEIGHT }); SetPosition({ g_window_info.restored_pad_x, g_window_info.restored_pad_y }); - SetSize({ g_window_info.restored_pad_width, g_window_info.restored_pad_height }); + if (g_window_info.restored_pad_width >= PAD_MIN_WIDTH && g_window_info.restored_pad_height >= PAD_MIN_HEIGHT) + SetClientSize({ g_window_info.restored_pad_width, g_window_info.restored_pad_height }); + else + SetClientSize(wxSize(854, 480)); if (g_window_info.pad_maximized) Maximize(); @@ -72,7 +78,7 @@ void PadViewFrame::InitializeRenderCanvas() m_render_canvas = GLCanvas_Create(this, wxSize(854, 480), false); sizer->Add(m_render_canvas, 1, wxEXPAND, 0, nullptr); } - SetSizerAndFit(sizer); + SetSizer(sizer); Layout(); m_render_canvas->Bind(wxEVT_KEY_UP, &PadViewFrame::OnKeyUp, this); From 5209677f2fd661949778c071287f5005b57bc8fe Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Tue, 2 Jul 2024 02:32:37 +0100 Subject: [PATCH 29/73] nsyshid: Add SetProtocol and SetReport support for libusb backend (#1243) --- src/Cafe/OS/libs/nsyshid/Backend.h | 2 +- src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp | 161 +++++++++++++----- src/Cafe/OS/libs/nsyshid/BackendLibusb.h | 15 +- .../OS/libs/nsyshid/BackendWindowsHID.cpp | 2 +- src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h | 2 +- src/Cafe/OS/libs/nsyshid/Skylander.cpp | 2 +- src/Cafe/OS/libs/nsyshid/Skylander.h | 2 +- src/Cafe/OS/libs/nsyshid/nsyshid.cpp | 4 +- 8 files changed, 137 insertions(+), 53 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/Backend.h b/src/Cafe/OS/libs/nsyshid/Backend.h index 03232736..12362773 100644 --- a/src/Cafe/OS/libs/nsyshid/Backend.h +++ b/src/Cafe/OS/libs/nsyshid/Backend.h @@ -135,7 +135,7 @@ namespace nsyshid uint8* output, uint32 outputMaxLength) = 0; - virtual bool SetProtocol(uint32 ifIndef, uint32 protocol) = 0; + virtual bool SetProtocol(uint8 ifIndex, uint8 protocol) = 0; virtual bool SetReport(ReportMessage* message) = 0; }; diff --git a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp index 6701d780..7548c998 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp @@ -230,6 +230,17 @@ namespace nsyshid::backend::libusb return nullptr; } + std::pair MakeConfigDescriptor(libusb_device* device, uint8 config_num) + { + libusb_config_descriptor* descriptor = nullptr; + const int ret = libusb_get_config_descriptor(device, config_num, &descriptor); + if (ret == LIBUSB_SUCCESS) + return {ret, ConfigDescriptor{descriptor, libusb_free_config_descriptor}}; + + return {ret, ConfigDescriptor{nullptr, [](auto) { + }}}; + } + std::shared_ptr BackendLibusb::CheckAndCreateDevice(libusb_device* dev) { struct libusb_device_descriptor desc; @@ -241,6 +252,25 @@ namespace nsyshid::backend::libusb ret); return nullptr; } + std::vector config_descriptors{}; + for (uint8 i = 0; i < desc.bNumConfigurations; ++i) + { + auto [ret, config_descriptor] = MakeConfigDescriptor(dev, i); + if (ret != LIBUSB_SUCCESS || !config_descriptor) + { + cemuLog_log(LogType::Force, "Failed to make config descriptor {} for {:04x}:{:04x}: {}", + i, desc.idVendor, desc.idProduct, libusb_error_name(ret)); + } + else + { + config_descriptors.emplace_back(std::move(config_descriptor)); + } + } + if (desc.idVendor == 0x0e6f && desc.idProduct == 0x0241) + { + cemuLog_logDebug(LogType::Force, + "nsyshid::BackendLibusb::CheckAndCreateDevice(): lego dimensions portal detected"); + } auto device = std::make_shared(m_ctx, desc.idVendor, desc.idProduct, @@ -248,7 +278,8 @@ namespace nsyshid::backend::libusb 2, 0, libusb_get_bus_number(dev), - libusb_get_device_address(dev)); + libusb_get_device_address(dev), + std::move(config_descriptors)); // figure out device endpoints if (!FindDefaultDeviceEndpoints(dev, device->m_libusbHasEndpointIn, @@ -330,7 +361,8 @@ namespace nsyshid::backend::libusb uint8 interfaceSubClass, uint8 protocol, uint8 libusbBusNumber, - uint8 libusbDeviceAddress) + uint8 libusbDeviceAddress, + std::vector configs) : Device(vendorId, productId, interfaceIndex, @@ -346,6 +378,7 @@ namespace nsyshid::backend::libusb m_libusbHasEndpointOut(false), m_libusbEndpointOut(0) { + m_config_descriptors = std::move(configs); } DeviceLibusb::~DeviceLibusb() @@ -413,20 +446,8 @@ namespace nsyshid::backend::libusb } this->m_handleInUseCounter = 0; } - if (libusb_kernel_driver_active(this->m_libusbHandle, 0) == 1) { - cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::open(): kernel driver active"); - if (libusb_detach_kernel_driver(this->m_libusbHandle, 0) == 0) - { - cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::open(): kernel driver detached"); - } - else - { - cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::open(): failed to detach kernel driver"); - } - } - { - int ret = libusb_claim_interface(this->m_libusbHandle, 0); + int ret = ClaimAllInterfaces(0); if (ret != 0) { cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::open(): cannot claim interface"); @@ -680,7 +701,65 @@ namespace nsyshid::backend::libusb return false; } - bool DeviceLibusb::SetProtocol(uint32 ifIndex, uint32 protocol) + template + static int DoForEachInterface(const Configs& configs, uint8 config_num, Function action) + { + int ret = LIBUSB_ERROR_NOT_FOUND; + if (configs.size() <= config_num || !configs[config_num]) + return ret; + for (uint8 i = 0; i < configs[config_num]->bNumInterfaces; ++i) + { + ret = action(i); + if (ret < LIBUSB_SUCCESS) + break; + } + return ret; + } + + int DeviceLibusb::ClaimAllInterfaces(uint8 config_num) + { + const int ret = DoForEachInterface(m_config_descriptors, config_num, [this](uint8 i) { + if (libusb_kernel_driver_active(this->m_libusbHandle, i)) + { + const int ret2 = libusb_detach_kernel_driver(this->m_libusbHandle, i); + if (ret2 < LIBUSB_SUCCESS && ret2 != LIBUSB_ERROR_NOT_FOUND && + ret2 != LIBUSB_ERROR_NOT_SUPPORTED) + { + cemuLog_log(LogType::Force, "Failed to detach kernel driver {}", libusb_error_name(ret2)); + return ret2; + } + } + return libusb_claim_interface(this->m_libusbHandle, i); + }); + if (ret < LIBUSB_SUCCESS) + { + cemuLog_log(LogType::Force, "Failed to release all interfaces for config {}", config_num); + } + return ret; + } + + int DeviceLibusb::ReleaseAllInterfaces(uint8 config_num) + { + const int ret = DoForEachInterface(m_config_descriptors, config_num, [this](uint8 i) { + return libusb_release_interface(AquireHandleLock()->GetHandle(), i); + }); + if (ret < LIBUSB_SUCCESS && ret != LIBUSB_ERROR_NO_DEVICE && ret != LIBUSB_ERROR_NOT_FOUND) + { + cemuLog_log(LogType::Force, "Failed to release all interfaces for config {}", config_num); + } + return ret; + } + + int DeviceLibusb::ReleaseAllInterfacesForCurrentConfig() + { + int config_num; + const int get_config_ret = libusb_get_configuration(AquireHandleLock()->GetHandle(), &config_num); + if (get_config_ret < LIBUSB_SUCCESS) + return get_config_ret; + return ReleaseAllInterfaces(config_num); + } + + bool DeviceLibusb::SetProtocol(uint8 ifIndex, uint8 protocol) { auto handleLock = AquireHandleLock(); if (!handleLock->IsValid()) @@ -688,24 +767,18 @@ namespace nsyshid::backend::libusb cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::SetProtocol(): device is not opened"); return false; } + if (m_interfaceIndex != ifIndex) + m_interfaceIndex = ifIndex; - // ToDo: implement this -#if 0 - // is this correct? Discarding "ifIndex" seems like a bad idea - int ret = libusb_set_configuration(handleLock->getHandle(), protocol); - if (ret == 0) { - cemuLog_logDebug(LogType::Force, - "nsyshid::DeviceLibusb::setProtocol(): success"); + ReleaseAllInterfacesForCurrentConfig(); + int ret = libusb_set_configuration(AquireHandleLock()->GetHandle(), protocol); + if (ret == LIBUSB_SUCCESS) + ret = ClaimAllInterfaces(protocol); + + if (ret == LIBUSB_SUCCESS) return true; - } - cemuLog_logDebug(LogType::Force, - "nsyshid::DeviceLibusb::setProtocol(): failed with error code: {}", - ret); - return false; -#endif - // pretend that everything is fine - return true; + return false; } bool DeviceLibusb::SetReport(ReportMessage* message) @@ -717,20 +790,20 @@ namespace nsyshid::backend::libusb return false; } - // ToDo: implement this -#if 0 - // not sure if libusb_control_transfer() is the right candidate for this - int ret = libusb_control_transfer(handleLock->getHandle(), - bmRequestType, - bRequest, - wValue, - wIndex, - message->reportData, - message->length, - timeout); -#endif + int ret = libusb_control_transfer(handleLock->GetHandle(), + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT, + LIBUSB_REQUEST_SET_CONFIGURATION, + 512, + 0, + message->originalData, + message->originalLength, + 0); - // pretend that everything is fine + if (ret != message->originalLength) + { + cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::SetReport(): Control Transfer Failed: {}", libusb_error_name(ret)); + return false; + } return true; } diff --git a/src/Cafe/OS/libs/nsyshid/BackendLibusb.h b/src/Cafe/OS/libs/nsyshid/BackendLibusb.h index a8122aff..a7b23769 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendLibusb.h +++ b/src/Cafe/OS/libs/nsyshid/BackendLibusb.h @@ -44,6 +44,11 @@ namespace nsyshid::backend::libusb bool& endpointOutFound, uint8& endpointOut, uint16& endpointOutMaxPacketSize); }; + template + using UniquePtr = std::unique_ptr; + + using ConfigDescriptor = UniquePtr; + class DeviceLibusb : public nsyshid::Device { public: DeviceLibusb(libusb_context* ctx, @@ -53,7 +58,8 @@ namespace nsyshid::backend::libusb uint8 interfaceSubClass, uint8 protocol, uint8 libusbBusNumber, - uint8 libusbDeviceAddress); + uint8 libusbDeviceAddress, + std::vector configs); ~DeviceLibusb() override; @@ -73,7 +79,11 @@ namespace nsyshid::backend::libusb uint8* output, uint32 outputMaxLength) override; - bool SetProtocol(uint32 ifIndex, uint32 protocol) override; + bool SetProtocol(uint8 ifIndex, uint8 protocol) override; + + int ClaimAllInterfaces(uint8 config_num); + int ReleaseAllInterfaces(uint8 config_num); + int ReleaseAllInterfacesForCurrentConfig(); bool SetReport(ReportMessage* message) override; @@ -92,6 +102,7 @@ namespace nsyshid::backend::libusb std::atomic m_handleInUseCounter; std::condition_variable m_handleInUseCounterDecremented; libusb_device_handle* m_libusbHandle; + std::vector m_config_descriptors; class HandleLock { public: diff --git a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp index 3cfba26a..44e01399 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp @@ -400,7 +400,7 @@ namespace nsyshid::backend::windows return false; } - bool DeviceWindowsHID::SetProtocol(uint32 ifIndef, uint32 protocol) + bool DeviceWindowsHID::SetProtocol(uint8 ifIndex, uint8 protocol) { // ToDo: implement this // pretend that everything is fine diff --git a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h index 84fe7bda..9a8a78e9 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h +++ b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.h @@ -47,7 +47,7 @@ namespace nsyshid::backend::windows bool GetDescriptor(uint8 descType, uint8 descIndex, uint8 lang, uint8* output, uint32 outputMaxLength) override; - bool SetProtocol(uint32 ifIndef, uint32 protocol) override; + bool SetProtocol(uint8 ifIndex, uint8 protocol) override; bool SetReport(ReportMessage* message) override; diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp index 241e1969..7f17f8a3 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.cpp +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -627,7 +627,7 @@ namespace nsyshid return true; } - bool SkylanderPortalDevice::SetProtocol(uint32 ifIndex, uint32 protocol) + bool SkylanderPortalDevice::SetProtocol(uint8 ifIndex, uint8 protocol) { return true; } diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h index a1ca7f8f..ae8b5d92 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.h +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -30,7 +30,7 @@ namespace nsyshid uint8* output, uint32 outputMaxLength) override; - bool SetProtocol(uint32 ifIndex, uint32 protocol) override; + bool SetProtocol(uint8 ifIndex, uint8 protocol) override; bool SetReport(ReportMessage* message) override; diff --git a/src/Cafe/OS/libs/nsyshid/nsyshid.cpp b/src/Cafe/OS/libs/nsyshid/nsyshid.cpp index c674b844..99a736d9 100644 --- a/src/Cafe/OS/libs/nsyshid/nsyshid.cpp +++ b/src/Cafe/OS/libs/nsyshid/nsyshid.cpp @@ -379,8 +379,8 @@ namespace nsyshid void export_HIDSetProtocol(PPCInterpreter_t* hCPU) { ppcDefineParamU32(hidHandle, 0); // r3 - ppcDefineParamU32(ifIndex, 1); // r4 - ppcDefineParamU32(protocol, 2); // r5 + ppcDefineParamU8(ifIndex, 1); // r4 + ppcDefineParamU8(protocol, 2); // r5 ppcDefineParamMPTR(callbackFuncMPTR, 3); // r6 ppcDefineParamMPTR(callbackParamMPTR, 4); // r7 cemuLog_logDebug(LogType::Force, "nsyshid.HIDSetProtocol(...)"); From 9d366937cd1c5908e073ecd726a0b12763838ef7 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 7 Jul 2024 08:55:26 +0200 Subject: [PATCH 30/73] Workaround for compiler issue with Visual Studio 17.10 --- src/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d5843c37..7d64d91b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -56,6 +56,12 @@ add_executable(CemuBin mainLLE.cpp ) +if(MSVC AND MSVC_VERSION EQUAL 1940) + # workaround for an msvc issue on VS 17.10 where generated ILK files are too large + # see https://developercommunity.visualstudio.com/t/After-updating-to-VS-1710-the-size-of-/10665511 + set_target_properties(CemuBin PROPERTIES LINK_FLAGS "/INCREMENTAL:NO") +endif() + if(WIN32) target_sources(CemuBin PRIVATE resource/cemu.rc From 7522c8470ee27d50a68ba662ae721b69018f3a8f Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:24:46 +0200 Subject: [PATCH 31/73] resource: move fontawesome to .rodata (#1259) --- src/resource/embedded/fontawesome.S | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource/embedded/fontawesome.S b/src/resource/embedded/fontawesome.S index 04da3b24..29b4f93a 100644 --- a/src/resource/embedded/fontawesome.S +++ b/src/resource/embedded/fontawesome.S @@ -1,4 +1,4 @@ -.section .text +.rodata .global g_fontawesome_data, g_fontawesome_size g_fontawesome_data: From 64232ffdbddd39cf14eed0ef457273af67277f3c Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 23 Jul 2024 03:13:36 +0200 Subject: [PATCH 32/73] Windows default to non-portable + Reworked MLC handling and related UI (#1252) --- src/config/ActiveSettings.cpp | 52 ++-- src/config/ActiveSettings.h | 23 +- src/config/CMakeLists.txt | 4 - src/config/CemuConfig.cpp | 18 -- src/config/CemuConfig.h | 2 +- src/config/PermanentConfig.cpp | 65 ----- src/config/PermanentConfig.h | 18 -- src/config/PermanentStorage.cpp | 76 ------ src/config/PermanentStorage.h | 27 -- src/gui/CemuApp.cpp | 407 +++++++++++++++++++------------ src/gui/CemuApp.h | 13 +- src/gui/GeneralSettings2.cpp | 154 ++++++------ src/gui/GeneralSettings2.h | 3 +- src/gui/GettingStartedDialog.cpp | 224 +++++++---------- src/gui/GettingStartedDialog.h | 34 +-- src/gui/MainWindow.cpp | 30 +-- src/gui/MainWindow.h | 2 - src/main.cpp | 14 +- 18 files changed, 515 insertions(+), 651 deletions(-) delete mode 100644 src/config/PermanentConfig.cpp delete mode 100644 src/config/PermanentConfig.h delete mode 100644 src/config/PermanentStorage.cpp delete mode 100644 src/config/PermanentStorage.h diff --git a/src/config/ActiveSettings.cpp b/src/config/ActiveSettings.cpp index 07e6f16d..560f2986 100644 --- a/src/config/ActiveSettings.cpp +++ b/src/config/ActiveSettings.cpp @@ -7,41 +7,47 @@ #include "config/LaunchSettings.h" #include "util/helpers/helpers.h" -std::set -ActiveSettings::LoadOnce( - const fs::path& executablePath, - const fs::path& userDataPath, - const fs::path& configPath, - const fs::path& cachePath, - const fs::path& dataPath) +void ActiveSettings::SetPaths(bool isPortableMode, + const fs::path& executablePath, + const fs::path& userDataPath, + const fs::path& configPath, + const fs::path& cachePath, + const fs::path& dataPath, + std::set& failedWriteAccess) { + cemu_assert_debug(!s_setPathsCalled); // can only change paths before loading + s_isPortableMode = isPortableMode; s_executable_path = executablePath; s_user_data_path = userDataPath; s_config_path = configPath; s_cache_path = cachePath; s_data_path = dataPath; - std::set failed_write_access; + failedWriteAccess.clear(); for (auto&& path : {userDataPath, configPath, cachePath}) { - if (!fs::exists(path)) - { - std::error_code ec; + std::error_code ec; + if (!fs::exists(path, ec)) fs::create_directories(path, ec); - } if (!TestWriteAccess(path)) { cemuLog_log(LogType::Force, "Failed to write to {}", _pathToUtf8(path)); - failed_write_access.insert(path); + failedWriteAccess.insert(path); } } - s_executable_filename = s_executable_path.filename(); + s_setPathsCalled = true; +} - g_config.SetFilename(GetConfigPath("settings.xml").generic_wstring()); - g_config.Load(); +[[nodiscard]] bool ActiveSettings::IsPortableMode() +{ + return s_isPortableMode; +} + +void ActiveSettings::Init() +{ + cemu_assert_debug(s_setPathsCalled); std::string additionalErrorInfo; s_has_required_online_files = iosuCrypt_checkRequirementsForOnlineMode(additionalErrorInfo) == IOS_CRYPTO_ONLINE_REQ_OK; - return failed_write_access; } bool ActiveSettings::LoadSharedLibrariesEnabled() @@ -229,6 +235,7 @@ bool ActiveSettings::ForceSamplerRoundToPrecision() fs::path ActiveSettings::GetMlcPath() { + cemu_assert_debug(s_setPathsCalled); if(const auto launch_mlc = LaunchSettings::GetMLCPath(); launch_mlc.has_value()) return launch_mlc.value(); @@ -238,6 +245,17 @@ fs::path ActiveSettings::GetMlcPath() return GetDefaultMLCPath(); } +bool ActiveSettings::IsCustomMlcPath() +{ + cemu_assert_debug(s_setPathsCalled); + return !GetConfig().mlc_path.GetValue().empty(); +} + +bool ActiveSettings::IsCommandLineMlcPath() +{ + return LaunchSettings::GetMLCPath().has_value(); +} + fs::path ActiveSettings::GetDefaultMLCPath() { return GetUserDataPath("mlc01"); diff --git a/src/config/ActiveSettings.h b/src/config/ActiveSettings.h index 54052741..e672fbee 100644 --- a/src/config/ActiveSettings.h +++ b/src/config/ActiveSettings.h @@ -34,12 +34,16 @@ private: public: // Set directories and return all directories that failed write access test - static std::set - LoadOnce(const fs::path& executablePath, - const fs::path& userDataPath, - const fs::path& configPath, - const fs::path& cachePath, - const fs::path& dataPath); + static void + SetPaths(bool isPortableMode, + const fs::path& executablePath, + const fs::path& userDataPath, + const fs::path& configPath, + const fs::path& cachePath, + const fs::path& dataPath, + std::set& failedWriteAccess); + + static void Init(); [[nodiscard]] static fs::path GetExecutablePath() { return s_executable_path; } [[nodiscard]] static fs::path GetExecutableFilename() { return s_executable_filename; } @@ -56,11 +60,14 @@ public: template [[nodiscard]] static fs::path GetMlcPath(TArgs&&... args){ return GetPath(GetMlcPath(), std::forward(args)...); }; + static bool IsCustomMlcPath(); + static bool IsCommandLineMlcPath(); // get mlc path to default cemu root dir/mlc01 [[nodiscard]] static fs::path GetDefaultMLCPath(); private: + inline static bool s_isPortableMode{false}; inline static fs::path s_executable_path; inline static fs::path s_user_data_path; inline static fs::path s_config_path; @@ -70,6 +77,9 @@ private: inline static fs::path s_mlc_path; public: + // can be called before Init + [[nodiscard]] static bool IsPortableMode(); + // general [[nodiscard]] static bool LoadSharedLibrariesEnabled(); [[nodiscard]] static bool DisplayDRCEnabled(); @@ -111,6 +121,7 @@ public: [[nodiscard]] static bool ForceSamplerRoundToPrecision(); private: + inline static bool s_setPathsCalled = false; // dump options inline static bool s_dump_shaders = false; inline static bool s_dump_textures = false; diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index f02b95d4..d53e8574 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -8,10 +8,6 @@ add_library(CemuConfig LaunchSettings.h NetworkSettings.cpp NetworkSettings.h - PermanentConfig.cpp - PermanentConfig.h - PermanentStorage.cpp - PermanentStorage.h XMLConfig.h ) diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 8e7cf398..03b12731 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -5,7 +5,6 @@ #include -#include "PermanentConfig.h" #include "ActiveSettings.h" XMLCemuConfig_t g_config(L"settings.xml"); @@ -15,23 +14,6 @@ void CemuConfig::SetMLCPath(fs::path path, bool save) mlc_path.SetValue(_pathToUtf8(path)); if(save) g_config.Save(); - - // if custom mlc path has been selected, store it in permanent config - if (path != ActiveSettings::GetDefaultMLCPath()) - { - try - { - auto pconfig = PermanentConfig::Load(); - pconfig.custom_mlc_path = _pathToUtf8(path); - pconfig.Store(); - } - catch (const PSDisabledException&) {} - catch (const std::exception& ex) - { - cemuLog_log(LogType::Force, "can't store custom mlc path in permanent storage: {}", ex.what()); - } - } - Account::RefreshAccounts(); } diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index d0776d2e..3f3da953 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -417,7 +417,7 @@ struct CemuConfig ConfigValue save_screenshot{true}; ConfigValue did_show_vulkan_warning{false}; - ConfigValue did_show_graphic_pack_download{false}; + ConfigValue did_show_graphic_pack_download{false}; // no longer used but we keep the config value around in case people downgrade Cemu. Despite the name this was used for the Getting Started dialog ConfigValue did_show_macos_disclaimer{false}; ConfigValue show_icon_column{ true }; diff --git a/src/config/PermanentConfig.cpp b/src/config/PermanentConfig.cpp deleted file mode 100644 index 20a44d28..00000000 --- a/src/config/PermanentConfig.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "PermanentConfig.h" - -#include "pugixml.hpp" - -#include "PermanentStorage.h" - -struct xml_string_writer : pugi::xml_writer -{ - std::string result; - - void write(const void* data, size_t size) override - { - result.append(static_cast(data), size); - } -}; - -std::string PermanentConfig::ToXMLString() const noexcept -{ - pugi::xml_document doc; - doc.append_child(pugi::node_declaration).append_attribute("encoding") = "UTF-8"; - auto root = doc.append_child("config"); - root.append_child("MlcPath").text().set(this->custom_mlc_path.c_str()); - - xml_string_writer writer; - doc.save(writer); - return writer.result; -} - -PermanentConfig PermanentConfig::FromXMLString(std::string_view str) noexcept -{ - PermanentConfig result{}; - - pugi::xml_document doc; - if(doc.load_buffer(str.data(), str.size())) - { - result.custom_mlc_path = doc.select_node("/config/MlcPath").node().text().as_string(); - } - - return result; -} - -PermanentConfig PermanentConfig::Load() -{ - PermanentStorage storage; - - const auto str = storage.ReadFile(kFileName); - if (!str.empty()) - return FromXMLString(str); - - return {}; -} - -bool PermanentConfig::Store() noexcept -{ - try - { - PermanentStorage storage; - storage.WriteStringToFile(kFileName, ToXMLString()); - } - catch (...) - { - return false; - } - return true; -} diff --git a/src/config/PermanentConfig.h b/src/config/PermanentConfig.h deleted file mode 100644 index 8c134747..00000000 --- a/src/config/PermanentConfig.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "PermanentStorage.h" - -struct PermanentConfig -{ - static constexpr const char* kFileName = "perm_setting.xml"; - - std::string custom_mlc_path; - - [[nodiscard]] std::string ToXMLString() const noexcept; - static PermanentConfig FromXMLString(std::string_view str) noexcept; - - // gets from permanent storage - static PermanentConfig Load(); - // saves to permanent storage - bool Store() noexcept; -}; diff --git a/src/config/PermanentStorage.cpp b/src/config/PermanentStorage.cpp deleted file mode 100644 index e095ff4b..00000000 --- a/src/config/PermanentStorage.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "PermanentStorage.h" -#include "config/CemuConfig.h" -#include "util/helpers/SystemException.h" - -PermanentStorage::PermanentStorage() -{ - if (!GetConfig().permanent_storage) - throw PSDisabledException(); - - const char* appdata = std::getenv("LOCALAPPDATA"); - if (!appdata) - throw std::runtime_error("can't get LOCALAPPDATA"); - m_storage_path = appdata; - m_storage_path /= "Cemu"; - - fs::create_directories(m_storage_path); -} - -PermanentStorage::~PermanentStorage() -{ - if (m_remove_storage) - { - std::error_code ec; - fs::remove_all(m_storage_path, ec); - if (ec) - { - SystemException ex(ec); - cemuLog_log(LogType::Force, "can't remove permanent storage: {}", ex.what()); - } - } -} - -void PermanentStorage::ClearAllFiles() const -{ - fs::remove_all(m_storage_path); - fs::create_directories(m_storage_path); -} - -void PermanentStorage::RemoveStorage() -{ - m_remove_storage = true; -} - -void PermanentStorage::WriteStringToFile(std::string_view filename, std::string_view content) -{ - const auto name = m_storage_path.append(filename.data(), filename.data() + filename.size()); - std::ofstream file(name.string()); - file.write(content.data(), (uint32_t)content.size()); -} - -std::string PermanentStorage::ReadFile(std::string_view filename) noexcept -{ - try - { - const auto name = m_storage_path.append(filename.data(), filename.data() + filename.size()); - std::ifstream file(name, std::ios::in | std::ios::ate); - if (!file.is_open()) - return {}; - - const auto end = file.tellg(); - file.seekg(0, std::ios::beg); - const auto file_size = end - file.tellg(); - if (file_size == 0) - return {}; - - std::string result; - result.resize(file_size); - file.read(result.data(), file_size); - return result; - } - catch (...) - { - return {}; - } - -} diff --git a/src/config/PermanentStorage.h b/src/config/PermanentStorage.h deleted file mode 100644 index 3cda3d6d..00000000 --- a/src/config/PermanentStorage.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -// disabled by config -class PSDisabledException : public std::runtime_error -{ -public: - PSDisabledException() - : std::runtime_error("permanent storage is disabled by user") {} -}; - -class PermanentStorage -{ -public: - PermanentStorage(); - ~PermanentStorage(); - - void ClearAllFiles() const; - // flags storage to be removed on destruction - void RemoveStorage(); - - void WriteStringToFile(std::string_view filename, std::string_view content); - std::string ReadFile(std::string_view filename) noexcept; - -private: - fs::path m_storage_path; - bool m_remove_storage = false; -}; \ No newline at end of file diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index 86d81e43..baa83888 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -3,11 +3,11 @@ #include "gui/wxgui.h" #include "config/CemuConfig.h" #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" +#include "Cafe/HW/Latte/Core/LatteOverlay.h" #include "gui/guiWrapper.h" #include "config/ActiveSettings.h" +#include "config/LaunchSettings.h" #include "gui/GettingStartedDialog.h" -#include "config/PermanentConfig.h" -#include "config/PermanentStorage.h" #include "input/InputManager.h" #include "gui/helpers/wxHelpers.h" #include "Cemu/ncrypto/ncrypto.h" @@ -30,7 +30,10 @@ wxIMPLEMENT_APP_NO_MAIN(CemuApp); extern WindowInfo g_window_info; extern std::shared_mutex g_mutex; -int mainEmulatorHLE(); +// forward declarations from main.cpp +void UnitTests(); +void CemuCommonInit(); + void HandlePostUpdate(); // Translation strings to extract for gettext: void unused_translation_dummy() @@ -54,34 +57,86 @@ void unused_translation_dummy() void(_("unknown")); } -bool CemuApp::OnInit() +#if BOOST_OS_WINDOWS +#include +fs::path GetAppDataRoamingPath() { + PWSTR path = nullptr; + HRESULT result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path); + if (result != S_OK || !path) + { + if (path) + CoTaskMemFree(path); + return {}; + } + std::string appDataPath = boost::nowide::narrow(path); + CoTaskMemFree(path); + return _utf8ToPath(appDataPath); +} +#endif + +#if BOOST_OS_WINDOWS +void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for Windows +{ + std::error_code ec; + bool isPortable = false; fs::path user_data_path, config_path, cache_path, data_path; auto standardPaths = wxStandardPaths::Get(); fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); + fs::path portablePath = exePath.parent_path() / "portable"; + data_path = exePath.parent_path(); // the data path is always the same as the exe path + if (fs::exists(portablePath, ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = portablePath; + } + else + { + fs::path roamingPath = GetAppDataRoamingPath() / "Cemu"; + user_data_path = config_path = cache_path = roamingPath; + } + // on Windows Cemu used to be portable by default prior to 2.0-89 + // to remain backwards compatible with old installations we check for settings.xml in the Cemu directory + // if it exists, we use the exe path as the portable directory + if(!isPortable) // lower priority than portable directory + { + if (fs::exists(exePath.parent_path() / "settings.xml", ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = exePath.parent_path(); + } + } + ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); +} +#endif + #if BOOST_OS_LINUX +void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for Linux +{ + std::error_code ec; + bool isPortable = false; + fs::path user_data_path, config_path, cache_path, data_path; + auto standardPaths = wxStandardPaths::Get(); + fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); + fs::path portablePath = exePath.parent_path() / "portable"; // GetExecutablePath returns the AppImage's temporary mount location wxString appImagePath; if (wxGetEnv(("APPIMAGE"), &appImagePath)) - exePath = wxHelper::MakeFSPath(appImagePath); -#endif - // Try a portable path first, if it exists. - user_data_path = config_path = cache_path = data_path = exePath.parent_path() / "portable"; -#if BOOST_OS_MACOS - // If run from an app bundle, use its parent directory. - fs::path appPath = exePath.parent_path().parent_path().parent_path(); - if (appPath.extension() == ".app") - user_data_path = config_path = cache_path = data_path = appPath.parent_path() / "portable"; -#endif - - if (!fs::exists(user_data_path)) { -#if BOOST_OS_WINDOWS - user_data_path = config_path = cache_path = data_path = exePath.parent_path(); -#else + exePath = wxHelper::MakeFSPath(appImagePath); + portablePath = exePath.parent_path() / "portable"; + } + if (fs::exists(portablePath, ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = portablePath; + // in portable mode assume the data directories (resources, gameProfiles/default/) are next to the executable + data_path = exePath.parent_path(); + } + else + { SetAppName("Cemu"); - wxString appName=GetAppName(); -#if BOOST_OS_LINUX + wxString appName = GetAppName(); standardPaths.SetFileLayout(wxStandardPaths::FileLayout::FileLayout_XDG); auto getEnvDir = [&](const wxString& varName, const wxString& defaultValue) { @@ -90,33 +145,151 @@ bool CemuApp::OnInit() return defaultValue; return dir; }; - wxString homeDir=wxFileName::GetHomeDir(); + wxString homeDir = wxFileName::GetHomeDir(); user_data_path = (getEnvDir(wxS("XDG_DATA_HOME"), homeDir + wxS("/.local/share")) + "/" + appName).ToStdString(); config_path = (getEnvDir(wxS("XDG_CONFIG_HOME"), homeDir + wxS("/.config")) + "/" + appName).ToStdString(); -#else - user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString(); -#endif data_path = standardPaths.GetDataDir().ToStdString(); cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString(); cache_path /= appName.ToStdString(); -#endif } + ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); +} +#endif - auto failed_write_access = ActiveSettings::LoadOnce(exePath, user_data_path, config_path, cache_path, data_path); - for (auto&& path : failed_write_access) - wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxString::FromUTF8(_pathToUtf8(path))), - _("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr); +#if BOOST_OS_MACOS +void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for MacOS +{ + std::error_code ec; + bool isPortable = false; + fs::path user_data_path, config_path, cache_path, data_path; + auto standardPaths = wxStandardPaths::Get(); + fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); + // If run from an app bundle, use its parent directory + fs::path appPath = exePath.parent_path().parent_path().parent_path(); + fs::path portablePath = appPath.extension() == ".app" ? appPath.parent_path() / "portable" : exePath.parent_path() / "portable"; + if (fs::exists(portablePath, ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = portablePath; + data_path = exePath.parent_path(); + } + else + { + SetAppName("Cemu"); + wxString appName = GetAppName(); + user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString(); + data_path = standardPaths.GetDataDir().ToStdString(); + cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString(); + cache_path /= appName.ToStdString(); + } + ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); +} +#endif + +// create default MLC files or quit if it fails +void CemuApp::InitializeNewMLCOrFail(fs::path mlc) +{ + if( CemuApp::CreateDefaultMLCFiles(mlc) ) + return; // all good + cemu_assert_debug(!ActiveSettings::IsCustomMlcPath()); // should not be possible? + + if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath()) + { + // tell user that the custom path is not writable + wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); + } + wxMessageBox(formatWxString(_("Cemu failed to write to the mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); +} + +void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) +{ + if(CreateDefaultMLCFiles(mlc)) + return; // all good + // failed to write mlc files + if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath()) + { + // tell user that the custom path is not writable + // if it's a command line path then just quit. Otherwise ask if user wants to reset the path + if(ActiveSettings::IsCommandLineMlcPath()) + { + wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); + } + // ask user if they want to reset the path + const wxString message = formatWxString(_("Cemu failed to write to the custom mlc directory.\n\nThe path is:\n{}\n\nCemu cannot start without a valid mlc path. Do you want to reset the path? You can later change it again in the General Settings."), + _pathToUtf8(mlc)); + wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxICON_WARNING); + dialog.SetYesNoLabels(_("Reset path"), _("Exit")); + const auto dialogResult = dialog.ShowModal(); + if (dialogResult == wxID_NO) + exit(0); + else // reset path + { + GetConfig().mlc_path = ""; + g_config.Save(); + } + } +} + +bool CemuApp::OnInit() +{ + std::set failedWriteAccess; + DeterminePaths(failedWriteAccess); + // make sure default cemu directories exist + CreateDefaultCemuFiles(); + + g_config.SetFilename(ActiveSettings::GetConfigPath("settings.xml").generic_wstring()); + + std::error_code ec; + bool isFirstStart = !fs::exists(ActiveSettings::GetConfigPath("settings.xml"), ec); NetworkConfig::LoadOnce(); - g_config.Load(); + if(!isFirstStart) + { + g_config.Load(); + LocalizeUI(static_cast(GetConfig().language == wxLANGUAGE_DEFAULT ? wxLocale::GetSystemLanguage() : GetConfig().language.GetValue())); + } + else + { + LocalizeUI(static_cast(wxLocale::GetSystemLanguage())); + } + for (auto&& path : failedWriteAccess) + { + wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxString::FromUTF8(_pathToUtf8(path))), + _("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr); + } + + if (isFirstStart) + { + // show the getting started dialog + GettingStartedDialog dia(nullptr); + dia.ShowModal(); + // make sure config is created. Gfx pack UI and input UI may create it earlier already, but we still want to update it + g_config.Save(); + // create mlc, on failure the user can quit here. So do this after the Getting Started dialog + InitializeNewMLCOrFail(ActiveSettings::GetMlcPath()); + } + else + { + // check if mlc is valid and recreate default files + InitializeExistingMLCOrFail(ActiveSettings::GetMlcPath()); + } + + ActiveSettings::Init(); // this is a bit of a misnomer, right now this call only loads certs for online play. In the future we should move the logic to a more appropriate place HandlePostUpdate(); - mainEmulatorHLE(); + + LatteOverlay_init(); + // run a couple of tests if in non-release mode +#ifdef CEMU_DEBUG_ASSERT + UnitTests(); +#endif + CemuCommonInit(); wxInitAllImageHandlers(); - LocalizeUI(); - // fill colour db wxTheColourDatabase->AddColour("ERROR", wxColour(0xCC, 0, 0)); wxTheColourDatabase->AddColour("SUCCESS", wxColour(0, 0xbb, 0)); @@ -135,15 +308,8 @@ bool CemuApp::OnInit() Bind(wxEVT_ACTIVATE_APP, &CemuApp::ActivateApp, this); auto& config = GetConfig(); - const bool first_start = !config.did_show_graphic_pack_download; - - CreateDefaultFiles(first_start); - m_mainFrame = new MainWindow(); - if (first_start) - m_mainFrame->ShowGettingStartedDialog(); - std::unique_lock lock(g_mutex); g_window_info.app_active = true; @@ -230,22 +396,22 @@ std::vector CemuApp::GetLanguages() const { return availableLanguages; } -void CemuApp::LocalizeUI() +void CemuApp::LocalizeUI(wxLanguage languageToUse) { std::unique_ptr translationsMgr(new wxTranslations()); m_availableTranslations = GetAvailableTranslationLanguages(translationsMgr.get()); - const sint32 configuredLanguage = GetConfig().language; bool isTranslationAvailable = std::any_of(m_availableTranslations.begin(), m_availableTranslations.end(), - [configuredLanguage](const wxLanguageInfo* info) { return info->Language == configuredLanguage; }); - if (configuredLanguage == wxLANGUAGE_DEFAULT || isTranslationAvailable) + [languageToUse](const wxLanguageInfo* info) { return info->Language == languageToUse; }); + if (languageToUse == wxLANGUAGE_DEFAULT || isTranslationAvailable) { - translationsMgr->SetLanguage(static_cast(configuredLanguage)); + translationsMgr->SetLanguage(static_cast(languageToUse)); translationsMgr->AddCatalog("cemu"); - if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(configuredLanguage)) - m_locale.Init(configuredLanguage); - + if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(languageToUse)) + { + m_locale.Init(languageToUse); + } // This must be run after wxLocale::Init, as the latter sets up its own wxTranslations instance which we want to override wxTranslations::Set(translationsMgr.release()); } @@ -264,55 +430,47 @@ std::vector CemuApp::GetAvailableTranslationLanguages(wxT return languages; } -void CemuApp::CreateDefaultFiles(bool first_start) +bool CemuApp::CheckMLCPath(const fs::path& mlc) { std::error_code ec; - fs::path mlc = ActiveSettings::GetMlcPath(); - // check for mlc01 folder missing if custom path has been set - if (!fs::exists(mlc, ec) && !first_start) - { - const wxString message = formatWxString(_("Your mlc01 folder seems to be missing.\n\nThis is where Cemu stores save files, game updates and other Wii U files.\n\nThe expected path is:\n{}\n\nDo you want to create the folder at the expected path?"), - _pathToUtf8(mlc)); - - wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxCANCEL| wxICON_WARNING); - dialog.SetYesNoCancelLabels(_("Yes"), _("No"), _("Select a custom path")); - const auto dialogResult = dialog.ShowModal(); - if (dialogResult == wxID_NO) - exit(0); - else if(dialogResult == wxID_CANCEL) - { - if (!SelectMLCPath()) - return; - mlc = ActiveSettings::GetMlcPath(); - } - else - { - GetConfig().mlc_path = ""; - g_config.Save(); - } - } + if (!fs::exists(mlc, ec)) + return false; + if (!fs::exists(mlc / "usr", ec) || !fs::exists(mlc / "sys", ec)) + return false; + return true; +} +bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc) +{ + auto CreateDirectoriesIfNotExist = [](const fs::path& path) + { + std::error_code ec; + if (!fs::exists(path, ec)) + return fs::create_directories(path, ec); + return true; + }; + // list of directories to create + const fs::path directories[] = { + mlc, + mlc / "sys", + mlc / "usr", + mlc / "usr/title/00050000", // base + mlc / "usr/title/0005000c", // dlc + mlc / "usr/title/0005000e", // update + mlc / "usr/save/00050010/1004a000/user/common/db", // Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200} + mlc / "usr/save/00050010/1004a100/user/common/db", + mlc / "usr/save/00050010/1004a200/user/common/db", + mlc / "sys/title/0005001b/1005c000/content" // lang files + }; + for(auto& path : directories) + { + if(!CreateDirectoriesIfNotExist(path)) + return false; + } // create sys/usr folder in mlc01 try { - const auto sysFolder = fs::path(mlc).append("sys"); - fs::create_directories(sysFolder); - - const auto usrFolder = fs::path(mlc).append("usr"); - fs::create_directories(usrFolder); - fs::create_directories(fs::path(usrFolder).append("title/00050000")); // base - fs::create_directories(fs::path(usrFolder).append("title/0005000c")); // dlc - fs::create_directories(fs::path(usrFolder).append("title/0005000e")); // update - - // Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200}, - fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a000/user/common/db")); - fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a100/user/common/db")); - fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a200/user/common/db")); - - // lang files const auto langDir = fs::path(mlc).append("sys/title/0005001b/1005c000/content"); - fs::create_directories(langDir); - auto langFile = fs::path(langDir).append("language.txt"); if (!fs::exists(langFile)) { @@ -346,18 +504,13 @@ void CemuApp::CreateDefaultFiles(bool first_start) } catch (const std::exception& ex) { - wxString errorMsg = formatWxString(_("Couldn't create a required mlc01 subfolder or file!\n\nError: {0}\nTarget path:\n{1}"), ex.what(), _pathToUtf8(mlc)); - -#if BOOST_OS_WINDOWS - const DWORD lastError = GetLastError(); - if (lastError != ERROR_SUCCESS) - errorMsg << fmt::format("\n\n{}", GetSystemErrorMessage(lastError)); -#endif - - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); + return false; } + return true; +} +void CemuApp::CreateDefaultCemuFiles() +{ // cemu directories try { @@ -384,58 +537,6 @@ void CemuApp::CreateDefaultFiles(bool first_start) } } - -bool CemuApp::TrySelectMLCPath(fs::path path) -{ - if (path.empty()) - path = ActiveSettings::GetDefaultMLCPath(); - - if (!TestWriteAccess(path)) - return false; - - GetConfig().SetMLCPath(path); - CemuApp::CreateDefaultFiles(); - - // update TitleList and SaveList scanner with new MLC path - CafeTitleList::SetMLCPath(path); - CafeTitleList::Refresh(); - CafeSaveList::SetMLCPath(path); - CafeSaveList::Refresh(); - return true; -} - -bool CemuApp::SelectMLCPath(wxWindow* parent) -{ - auto& config = GetConfig(); - - fs::path default_path; - if (fs::exists(_utf8ToPath(config.mlc_path.GetValue()))) - default_path = _utf8ToPath(config.mlc_path.GetValue()); - - // try until users selects a valid path or aborts - while(true) - { - wxDirDialog path_dialog(parent, _("Select a mlc directory"), wxHelper::FromPath(default_path), wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty()) - return false; - - const auto path = path_dialog.GetPath().ToStdWstring(); - - if (!TrySelectMLCPath(path)) - { - const auto result = wxMessageBox(_("Cemu can't write to the selected mlc path!\nDo you want to select another path?"), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR); - if (result == wxYES) - continue; - - break; - } - - return true; - } - - return false; -} - void CemuApp::ActivateApp(wxActivateEvent& event) { g_window_info.app_active = event.GetActive(); diff --git a/src/gui/CemuApp.h b/src/gui/CemuApp.h index cfdab0a2..b73d627d 100644 --- a/src/gui/CemuApp.h +++ b/src/gui/CemuApp.h @@ -15,13 +15,18 @@ public: std::vector GetLanguages() const; - static void CreateDefaultFiles(bool first_start = false); - static bool TrySelectMLCPath(fs::path path); - static bool SelectMLCPath(wxWindow* parent = nullptr); + static bool CheckMLCPath(const fs::path& mlc); + static bool CreateDefaultMLCFiles(const fs::path& mlc); + static void CreateDefaultCemuFiles(); + static void InitializeNewMLCOrFail(fs::path mlc); + static void InitializeExistingMLCOrFail(fs::path mlc); private: + void LocalizeUI(wxLanguage languageToUse); + + void DeterminePaths(std::set& failedWriteAccess); + void ActivateApp(wxActivateEvent& event); - void LocalizeUI(); static std::vector GetAvailableTranslationLanguages(wxTranslations* translationsMgr); MainWindow* m_mainFrame = nullptr; diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index c0b54949..08395cd3 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -32,7 +32,6 @@ #include #include "util/helpers/SystemException.h" #include "gui/dialogs/CreateAccount/wxCreateAccountDialog.h" -#include "config/PermanentStorage.h" #if BOOST_OS_WINDOWS #include @@ -176,19 +175,15 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) 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); - m_permanent_storage = new wxCheckBox(box, wxID_ANY, _("Use permanent storage")); - m_permanent_storage->SetToolTip(_("Cemu will remember your custom mlc path in %LOCALAPPDATA%/Cemu for new installations.")); - second_row->Add(m_permanent_storage, 0, botflag, 5); - second_row->AddSpacer(10); 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); - // Enable/disable feral interactive gamemode + // 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); + 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); #endif // temporary workaround because feature crashes on macOS @@ -203,23 +198,33 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) } { - auto* box = new wxStaticBox(panel, wxID_ANY, _("MLC Path")); - auto* box_sizer = new wxStaticBoxSizer(box, wxHORIZONTAL); + auto* outerMlcBox = new wxStaticBox(panel, wxID_ANY, _("Custom MLC path")); - m_mlc_path = new wxTextCtrl(box, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY); + auto* box_sizer_mlc = new wxStaticBoxSizer(outerMlcBox, wxVERTICAL); + box_sizer_mlc->Add(new wxStaticText(box_sizer_mlc->GetStaticBox(), wxID_ANY, _("You can configure a custom path for the emulated internal Wii U storage (MLC).\nThis is where Cemu stores saves, accounts and other Wii U system files."), wxDefaultPosition, wxDefaultSize, 0), 0, wxALL, 5); + + auto* mlcPathLineSizer = new wxBoxSizer(wxHORIZONTAL); + + m_mlc_path = new wxTextCtrl(outerMlcBox, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY); m_mlc_path->SetMinSize(wxSize(150, -1)); - m_mlc_path->Bind(wxEVT_CHAR, &GeneralSettings2::OnMLCPathChar, this); m_mlc_path->SetToolTip(_("The mlc directory contains your save games and installed game update/dlc data")); - box_sizer->Add(m_mlc_path, 1, wxALL | wxEXPAND, 5); + mlcPathLineSizer->Add(m_mlc_path, 1, wxALL | wxEXPAND, 5); - auto* change_path = new wxButton(box, wxID_ANY, "..."); - change_path->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathSelect, this); - change_path->SetToolTip(_("Select a custom mlc path\nThe mlc path is used to store Wii U related files like save games, game updates and dlc data")); - box_sizer->Add(change_path, 0, wxALL, 5); + auto* changePath = new wxButton(outerMlcBox, wxID_ANY, "Change"); + changePath->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathSelect, this); + mlcPathLineSizer->Add(changePath, 0, wxALL, 5); if (LaunchSettings::GetMLCPath().has_value()) - change_path->Disable(); - general_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); + changePath->Disable(); + + auto* clearPath = new wxButton(outerMlcBox, wxID_ANY, "Clear custom path"); + clearPath->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathClear, this); + mlcPathLineSizer->Add(clearPath, 0, wxALL, 5); + if (LaunchSettings::GetMLCPath().has_value() || !ActiveSettings::IsCustomMlcPath()) + clearPath->Disable(); + + box_sizer_mlc->Add(mlcPathLineSizer, 0, wxEXPAND, 5); + general_panel_sizer->Add(box_sizer_mlc, 0, wxEXPAND | wxALL, 5); } { @@ -897,39 +902,12 @@ void GeneralSettings2::StoreConfig() #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) config.feral_gamemode = m_feral_gamemode->IsChecked(); #endif - const bool use_ps = m_permanent_storage->IsChecked(); - if(use_ps) - { - config.permanent_storage = use_ps; - try - { - - PermanentStorage storage; - storage.RemoveStorage(); - } - catch (...) {} - } - else - { - try - { - // delete permanent storage - PermanentStorage storage; - storage.RemoveStorage(); - } - catch (...) {} - config.permanent_storage = use_ps; - } - config.disable_screensaver = m_disable_screensaver->IsChecked(); // Toggle while a game is running if (CafeSystem::IsTitleRunning()) { ScreenSaver::SetInhibit(config.disable_screensaver); } - - if (!LaunchSettings::GetMLCPath().has_value()) - config.SetMLCPath(wxHelper::MakeFSPath(m_mlc_path->GetValue()), false); // -1 is default wx widget value -> set to dummy 0 so mainwindow and padwindow will update it config.window_position = m_save_window_position_size->IsChecked() ? Vector2i{ 0,0 } : Vector2i{-1,-1}; @@ -1560,7 +1538,6 @@ void GeneralSettings2::ApplyConfig() m_auto_update->SetValue(config.check_update); m_save_screenshot->SetValue(config.save_screenshot); - m_permanent_storage->SetValue(config.permanent_storage); m_disable_screensaver->SetValue(config.disable_screensaver); #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) m_feral_gamemode->SetValue(config.feral_gamemode); @@ -1570,6 +1547,7 @@ void GeneralSettings2::ApplyConfig() m_disable_screensaver->SetValue(false); #endif + m_game_paths->Clear(); for (auto& path : config.game_paths) { m_game_paths->Append(to_wxString(path)); @@ -1985,34 +1963,70 @@ void GeneralSettings2::OnAccountServiceChanged(wxCommandEvent& event) void GeneralSettings2::OnMLCPathSelect(wxCommandEvent& event) { - if (!CemuApp::SelectMLCPath(this)) + if(CafeSystem::IsTitleRunning()) + { + wxMessageBox(_("Can't change MLC path while a game is running!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); return; - - m_mlc_path->SetValue(wxHelper::FromPath(ActiveSettings::GetMlcPath())); - m_reload_gamelist = true; - m_mlc_modified = true; + } + // show directory dialog + wxDirDialog path_dialog(this, _("Select MLC directory"), wxEmptyString, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty()) + return; + // check if the choosen MLC path is an already initialized MLC location + fs::path newMlc = wxHelper::MakeFSPath(path_dialog.GetPath()); + if(CemuApp::CheckMLCPath(newMlc)) + { + // ask user if they are sure they want to use this folder and let them know that accounts and saves wont transfer + wxString message = _("Note that changing the MLC location will not transfer any accounts or save files. Are you sure you want to change the path?"); + wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); + if(dialog.ShowModal() == wxID_NO) + return; + if( !CemuApp::CreateDefaultMLCFiles(newMlc) ) // creating also acts as a check for read+write access + { + wxMessageBox(_("Failed to create default MLC files in the selected directory. The MLC path has not been changed"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); + return; + } + } + else + { + // ask user if they want to create a new mlc structure at the choosen location + wxString message = _("The selected directory does not contain the expected MLC structure. Do you want to create a new MLC structure in this directory?\nNote that changing the MLC location will not transfer any accounts or save files."); + wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); + if( !CemuApp::CreateDefaultMLCFiles(newMlc) ) + { + wxMessageBox(_("Failed to create default MLC files in the selected directory. The MLC path has not been changed"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); + return; + } + } + // update MLC path and store any other modified settings + GetConfig().SetMLCPath(newMlc); + StoreConfig(); + wxMessageBox(_("Cemu needs to be restarted for the changes to take effect."), _("Information"), wxOK | wxCENTRE | wxICON_INFORMATION, this); + // close settings and then cemu + wxCloseEvent closeEvent(wxEVT_CLOSE_WINDOW); + wxPostEvent(this, closeEvent); + wxPostEvent(GetParent(), closeEvent); } -void GeneralSettings2::OnMLCPathChar(wxKeyEvent& event) +void GeneralSettings2::OnMLCPathClear(wxCommandEvent& event) { - if (LaunchSettings::GetMLCPath().has_value()) - return; - - if(event.GetKeyCode() == WXK_DELETE || event.GetKeyCode() == WXK_BACK) + if(CafeSystem::IsTitleRunning()) { - fs::path newPath = ""; - if(!CemuApp::TrySelectMLCPath(newPath)) - { - const auto res = wxMessageBox(_("The default MLC path is inaccessible.\nDo you want to select a different path?"), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR); - if (res == wxYES && CemuApp::SelectMLCPath(this)) - newPath = ActiveSettings::GetMlcPath(); - else - return; - } - m_mlc_path->SetValue(wxHelper::FromPath(newPath)); - m_reload_gamelist = true; - m_mlc_modified = true; + wxMessageBox(_("Can't change MLC path while a game is running!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); + return; } + wxString message = _("Note that changing the MLC location will not transfer any accounts or save files. Are you sure you want to change the path?"); + wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); + if(dialog.ShowModal() == wxID_NO) + return; + GetConfig().SetMLCPath(""); + StoreConfig(); + g_config.Save(); + wxMessageBox(_("Cemu needs to be restarted for the changes to take effect."), _("Information"), wxOK | wxCENTRE | wxICON_INFORMATION, this); + // close settings and then cemu + wxCloseEvent closeEvent(wxEVT_CLOSE_WINDOW); + wxPostEvent(this, closeEvent); + wxPostEvent(GetParent(), closeEvent); } void GeneralSettings2::OnShowOnlineValidator(wxCommandEvent& event) diff --git a/src/gui/GeneralSettings2.h b/src/gui/GeneralSettings2.h index b34c9222..a3429fa1 100644 --- a/src/gui/GeneralSettings2.h +++ b/src/gui/GeneralSettings2.h @@ -42,7 +42,6 @@ private: wxCheckBox* m_save_padwindow_position_size; wxCheckBox* m_discord_presence, *m_fullscreen_menubar; wxCheckBox* m_auto_update, *m_save_screenshot; - wxCheckBox* m_permanent_storage; wxCheckBox* m_disable_screensaver; #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) wxCheckBox* m_feral_gamemode; @@ -96,7 +95,7 @@ private: void OnRemovePathClicked(wxCommandEvent& event); void OnActiveAccountChanged(wxCommandEvent& event); void OnMLCPathSelect(wxCommandEvent& event); - void OnMLCPathChar(wxKeyEvent& event); + void OnMLCPathClear(wxCommandEvent& event); void OnShowOnlineValidator(wxCommandEvent& event); void OnAccountServiceChanged(wxCommandEvent& event); static wxString GetOnlineAccountErrorMessage(OnlineAccountError error); diff --git a/src/gui/GettingStartedDialog.cpp b/src/gui/GettingStartedDialog.cpp index bfd206b1..22426cf2 100644 --- a/src/gui/GettingStartedDialog.cpp +++ b/src/gui/GettingStartedDialog.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "config/ActiveSettings.h" #include "gui/CemuApp.h" @@ -11,7 +12,6 @@ #include "gui/GraphicPacksWindow2.h" #include "gui/input/InputSettings2.h" #include "config/CemuConfig.h" -#include "config/PermanentConfig.h" #include "Cafe/TitleList/TitleList.h" @@ -21,75 +21,100 @@ #include "wxHelper.h" +wxDEFINE_EVENT(EVT_REFRESH_FIRST_PAGE, wxCommandEvent); // used to refresh the first page after the language change + wxPanel* GettingStartedDialog::CreatePage1() { - auto* result = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + auto* mainPanel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); auto* page1_sizer = new wxBoxSizer(wxVERTICAL); { auto* sizer = new wxBoxSizer(wxHORIZONTAL); - - sizer->Add(new wxStaticBitmap(result, wxID_ANY, wxICON(M_WND_ICON128)), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* m_staticText11 = new wxStaticText(result, wxID_ANY, _("It looks like you're starting Cemu for the first time.\nThis quick setup assistant will help you get the best experience"), wxDefaultPosition, wxDefaultSize, 0); - m_staticText11->Wrap(-1); - sizer->Add(m_staticText11, 0, wxALL, 5); - + sizer->Add(new wxStaticBitmap(mainPanel, wxID_ANY, wxICON(M_WND_ICON128)), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + m_page1.staticText11 = new wxStaticText(mainPanel, wxID_ANY, _("It looks like you're starting Cemu for the first time.\nThis quick setup assistant will help you get the best experience"), wxDefaultPosition, wxDefaultSize, 0); + m_page1.staticText11->Wrap(-1); + sizer->Add(m_page1.staticText11, 0, wxALL, 5); page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); } + if(ActiveSettings::IsPortableMode()) { - m_mlc_box_sizer = new wxStaticBoxSizer(wxVERTICAL, result, _("mlc01 path")); - m_mlc_box_sizer->Add(new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("The mlc path is the root folder of the emulated Wii U internal flash storage. It contains all your saves, installed updates and DLCs.\nIt is strongly recommend that you create a dedicated folder for it (example: C:\\wiiu\\mlc\\) \nIf left empty, the mlc folder will be created inside the Cemu folder.")), 0, wxALL, 5); + m_page1.portableModeInfoText = new wxStaticText(mainPanel, wxID_ANY, _("Cemu is running in portable mode")); + m_page1.portableModeInfoText->Show(true); + page1_sizer->Add(m_page1.portableModeInfoText, 0, wxALL, 5); - m_prev_mlc_warning = new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("A custom mlc path from a previous Cemu installation has been found and filled in.")); - m_prev_mlc_warning->SetForegroundColour(*wxRED); - m_prev_mlc_warning->Show(false); - m_mlc_box_sizer->Add(m_prev_mlc_warning, 0, wxALL, 5); - - auto* mlc_path_sizer = new wxBoxSizer(wxHORIZONTAL); - mlc_path_sizer->Add(new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("Custom mlc01 path")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - // workaround since we can't specify our own browse label? >> _("Browse") - m_mlc_folder = new wxDirPickerCtrl(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder"), wxDefaultPosition, wxDefaultSize, wxDIRP_DEFAULT_STYLE); - auto tTest1 = m_mlc_folder->GetTextCtrl(); - if(m_mlc_folder->HasTextCtrl()) - { - m_mlc_folder->GetTextCtrl()->SetEditable(false); - m_mlc_folder->GetTextCtrl()->Bind(wxEVT_CHAR, &GettingStartedDialog::OnMLCPathChar, this); - } - mlc_path_sizer->Add(m_mlc_folder, 1, wxALL, 5); - - mlc_path_sizer->Add(new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("(optional)")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_mlc_box_sizer->Add(mlc_path_sizer, 0, wxEXPAND, 5); - - page1_sizer->Add(m_mlc_box_sizer, 0, wxALL | wxEXPAND, 5); } + // language selection +#if 0 { - auto* sizer = new wxStaticBoxSizer(wxVERTICAL, result, _("Game paths")); + m_page1.languageBoxSizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Language")); + m_page1.languageText = new wxStaticText(m_page1.languageBoxSizer->GetStaticBox(), wxID_ANY, _("Select the language you want to use in Cemu")); + m_page1.languageBoxSizer->Add(m_page1.languageText, 0, wxALL, 5); - sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("The game path is scanned by Cemu to locate your games. We recommend creating a dedicated directory in which\nyou place all your Wii U games. (example: C:\\wiiu\\games\\)\n\nYou can also set additional paths in the general settings of Cemu.")), 0, wxALL, 5); + wxString language_choices[] = { _("Default") }; + wxChoice* m_language = new wxChoice(m_page1.languageBoxSizer->GetStaticBox(), wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(language_choices), language_choices); + m_language->SetSelection(0); + + for (const auto& language : wxGetApp().GetLanguages()) + { + m_language->Append(language->DescriptionNative); + } + + m_language->SetSelection(0); + m_page1.languageBoxSizer->Add(m_language, 0, wxALL | wxEXPAND, 5); + + page1_sizer->Add(m_page1.languageBoxSizer, 0, wxALL | wxEXPAND, 5); + + m_language->Bind(wxEVT_CHOICE, [this, m_language](const auto&) + { + const auto language = m_language->GetStringSelection(); + auto selection = m_language->GetSelection(); + if (selection == 0) + GetConfig().language = wxLANGUAGE_DEFAULT; + else + { + auto* app = (CemuApp*)wxTheApp; + const auto language = m_language->GetStringSelection(); + for (const auto& lang : app->GetLanguages()) + { + if (lang->DescriptionNative == language) + { + app->LocalizeUI(static_cast(lang->Language)); + wxCommandEvent event(EVT_REFRESH_FIRST_PAGE); + wxPostEvent(this, event); + break; + } + } + } + }); + } +#endif + + { + m_page1.gamePathBoxSizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Game paths")); + m_page1.gamePathText = new wxStaticText(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, _("The game path is scanned by Cemu to automatically locate your games, game updates and DLCs. We recommend creating a dedicated directory in which\nyou place all your Wii U game files. Additional paths can be set later in Cemu's general settings. All common Wii U game formats are supported by Cemu.")); + m_page1.gamePathBoxSizer->Add(m_page1.gamePathText, 0, wxALL, 5); auto* game_path_sizer = new wxBoxSizer(wxHORIZONTAL); - game_path_sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Game path")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + m_page1.gamePathText2 = new wxStaticText(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, _("Game path")); + game_path_sizer->Add(m_page1.gamePathText2, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_game_path = new wxDirPickerCtrl(sizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder")); - game_path_sizer->Add(m_game_path, 1, wxALL, 5); + m_page1.gamePathPicker = new wxDirPickerCtrl(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder")); + game_path_sizer->Add(m_page1.gamePathPicker, 1, wxALL, 5); - sizer->Add(game_path_sizer, 0, wxEXPAND, 5); + m_page1.gamePathBoxSizer->Add(game_path_sizer, 0, wxEXPAND, 5); - page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); + page1_sizer->Add(m_page1.gamePathBoxSizer, 0, wxALL | wxEXPAND, 5); } { - auto* sizer = new wxStaticBoxSizer(wxVERTICAL, result, _("Graphic packs")); + auto* sizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Graphic packs && mods")); - sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Graphic packs improve games by offering the possibility to change resolution, tweak FPS or add other visual or gameplay modifications.\nDownload the community graphic packs to get started.\n")), 0, wxALL, 5); + sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Graphic packs improve games by offering the ability to change resolution, increase FPS, tweak visuals or add gameplay modifications.\nGet started by opening the graphic packs configuration window.\n")), 0, wxALL, 5); - auto* download_gp = new wxButton(sizer->GetStaticBox(), wxID_ANY, _("Download community graphic packs")); - download_gp->Bind(wxEVT_BUTTON, &GettingStartedDialog::OnDownloadGPs, this); + auto* download_gp = new wxButton(sizer->GetStaticBox(), wxID_ANY, _("Download and configure graphic packs")); + download_gp->Bind(wxEVT_BUTTON, &GettingStartedDialog::OnConfigureGPs, this); sizer->Add(download_gp, 0, wxALIGN_CENTER | wxALL, 5); page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); @@ -102,16 +127,15 @@ wxPanel* GettingStartedDialog::CreatePage1() sizer->SetFlexibleDirection(wxBOTH); sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_ALL); - auto* next = new wxButton(result, wxID_ANY, _("Next"), wxDefaultPosition, wxDefaultSize, 0); + auto* next = new wxButton(mainPanel, wxID_ANY, _("Next"), wxDefaultPosition, wxDefaultSize, 0); next->Bind(wxEVT_BUTTON, [this](const auto&){m_notebook->SetSelection(1); }); sizer->Add(next, 0, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); page1_sizer->Add(sizer, 1, wxEXPAND, 5); } - - result->SetSizer(page1_sizer); - return result; + mainPanel->SetSizer(page1_sizer); + return mainPanel; } wxPanel* GettingStartedDialog::CreatePage2() @@ -138,17 +162,17 @@ wxPanel* GettingStartedDialog::CreatePage2() option_sizer->SetFlexibleDirection(wxBOTH); option_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - m_fullscreen = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Start games with fullscreen")); - option_sizer->Add(m_fullscreen, 0, wxALL, 5); + m_page2.fullscreenCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Start games with fullscreen")); + option_sizer->Add(m_page2.fullscreenCheckbox, 0, wxALL, 5); - m_separate = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Open separate pad screen")); - option_sizer->Add(m_separate, 0, wxALL, 5); + m_page2.separateCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Open separate pad screen")); + option_sizer->Add(m_page2.separateCheckbox, 0, wxALL, 5); - m_update = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Automatically check for updates")); - option_sizer->Add(m_update, 0, wxALL, 5); + m_page2.updateCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Automatically check for updates")); + option_sizer->Add(m_page2.updateCheckbox, 0, wxALL, 5); #if BOOST_OS_LINUX if (!std::getenv("APPIMAGE")) { - m_update->Disable(); + m_page2.updateCheckbox->Disable(); } #endif sizer->Add(option_sizer, 1, wxEXPAND, 5); @@ -162,10 +186,6 @@ wxPanel* GettingStartedDialog::CreatePage2() sizer->SetFlexibleDirection(wxBOTH); sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_ALL); - m_dont_show = new wxCheckBox(result, wxID_ANY, _("Don't show this again")); - m_dont_show->SetValue(true); - sizer->Add(m_dont_show, 0, wxALIGN_BOTTOM | wxALL, 5); - auto* previous = new wxButton(result, wxID_ANY, _("Previous")); previous->Bind(wxEVT_BUTTON, [this](const auto&) {m_notebook->SetSelection(0); }); sizer->Add(previous, 0, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); @@ -184,23 +204,9 @@ wxPanel* GettingStartedDialog::CreatePage2() void GettingStartedDialog::ApplySettings() { auto& config = GetConfig(); - m_fullscreen->SetValue(config.fullscreen.GetValue()); - m_update->SetValue(config.check_update.GetValue()); - m_separate->SetValue(config.pad_open.GetValue()); - m_dont_show->SetValue(true); // we want it always enabled by default - m_mlc_folder->SetPath(config.mlc_path.GetValue()); - - try - { - const auto pconfig = PermanentConfig::Load(); - if(!pconfig.custom_mlc_path.empty()) - { - m_mlc_folder->SetPath(wxString::FromUTF8(pconfig.custom_mlc_path)); - m_prev_mlc_warning->Show(true); - } - } - catch (const PSDisabledException&) {} - catch (...) {} + m_page2.fullscreenCheckbox->SetValue(config.fullscreen.GetValue()); + m_page2.updateCheckbox->SetValue(config.check_update.GetValue()); + m_page2.separateCheckbox->SetValue(config.pad_open.GetValue()); } void GettingStartedDialog::UpdateWindowSize() @@ -219,46 +225,25 @@ void GettingStartedDialog::OnClose(wxCloseEvent& event) event.Skip(); auto& config = GetConfig(); - config.fullscreen = m_fullscreen->GetValue(); - config.check_update = m_update->GetValue(); - config.pad_open = m_separate->GetValue(); - config.did_show_graphic_pack_download = m_dont_show->GetValue(); + config.fullscreen = m_page2.fullscreenCheckbox->GetValue(); + config.check_update = m_page2.updateCheckbox->GetValue(); + config.pad_open = m_page2.separateCheckbox->GetValue(); - const fs::path gamePath = wxHelper::MakeFSPath(m_game_path->GetPath()); - if (!gamePath.empty() && fs::exists(gamePath)) + const fs::path gamePath = wxHelper::MakeFSPath(m_page1.gamePathPicker->GetPath()); + std::error_code ec; + if (!gamePath.empty() && fs::exists(gamePath, ec)) { const auto it = std::find(config.game_paths.cbegin(), config.game_paths.cend(), gamePath); if (it == config.game_paths.cend()) { config.game_paths.emplace_back(_pathToUtf8(gamePath)); - m_game_path_changed = true; } } - - const fs::path mlcPath = wxHelper::MakeFSPath(m_mlc_folder->GetPath()); - if(config.mlc_path.GetValue() != mlcPath && (mlcPath.empty() || fs::exists(mlcPath))) - { - config.SetMLCPath(mlcPath, false); - m_mlc_changed = true; - } - - g_config.Save(); - - if(m_mlc_changed) - CemuApp::CreateDefaultFiles(); - - CafeTitleList::ClearScanPaths(); - for (auto& it : GetConfig().game_paths) - CafeTitleList::AddScanPath(_utf8ToPath(it)); - CafeTitleList::Refresh(); } - GettingStartedDialog::GettingStartedDialog(wxWindow* parent) : wxDialog(parent, wxID_ANY, _("Getting started"), wxDefaultPosition, { 740,530 }, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { - //this->SetSizeHints(wxDefaultSize, { 740,530 }); - auto* sizer = new wxBoxSizer(wxVERTICAL); m_notebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0); @@ -274,24 +259,18 @@ GettingStartedDialog::GettingStartedDialog(wxWindow* parent) this->SetSizer(sizer); this->Centre(wxBOTH); this->Bind(wxEVT_CLOSE_WINDOW, &GettingStartedDialog::OnClose, this); - + ApplySettings(); UpdateWindowSize(); } -void GettingStartedDialog::OnDownloadGPs(wxCommandEvent& event) +void GettingStartedDialog::OnConfigureGPs(wxCommandEvent& event) { DownloadGraphicPacksWindow dialog(this); dialog.ShowModal(); - GraphicPacksWindow2::RefreshGraphicPacks(); - - wxMessageDialog ask_dialog(this, _("Do you want to view the downloaded graphic packs?"), _("Graphic packs"), wxCENTRE | wxYES_NO); - if (ask_dialog.ShowModal() == wxID_YES) - { - GraphicPacksWindow2 window(this, 0); - window.ShowModal(); - } + GraphicPacksWindow2 window(this, 0); + window.ShowModal(); } void GettingStartedDialog::OnInputSettings(wxCommandEvent& event) @@ -299,20 +278,3 @@ void GettingStartedDialog::OnInputSettings(wxCommandEvent& event) InputSettings2 dialog(this); dialog.ShowModal(); } - -void GettingStartedDialog::OnMLCPathChar(wxKeyEvent& event) -{ - //if (LaunchSettings::GetMLCPath().has_value()) - // return; - - if (event.GetKeyCode() == WXK_DELETE || event.GetKeyCode() == WXK_BACK) - { - m_mlc_folder->GetTextCtrl()->SetValue(wxEmptyString); - if(m_prev_mlc_warning->IsShown()) - { - m_prev_mlc_warning->Show(false); - UpdateWindowSize(); - } - } -} - diff --git a/src/gui/GettingStartedDialog.h b/src/gui/GettingStartedDialog.h index ec122eab..9dfd69b4 100644 --- a/src/gui/GettingStartedDialog.h +++ b/src/gui/GettingStartedDialog.h @@ -13,9 +13,6 @@ class GettingStartedDialog : public wxDialog public: GettingStartedDialog(wxWindow* parent = nullptr); - [[nodiscard]] bool HasGamePathChanged() const { return m_game_path_changed; } - [[nodiscard]] bool HasMLCChanged() const { return m_mlc_changed; } - private: wxPanel* CreatePage1(); wxPanel* CreatePage2(); @@ -23,22 +20,29 @@ private: void UpdateWindowSize(); void OnClose(wxCloseEvent& event); - void OnDownloadGPs(wxCommandEvent& event); + void OnConfigureGPs(wxCommandEvent& event); void OnInputSettings(wxCommandEvent& event); - void OnMLCPathChar(wxKeyEvent& event); wxSimplebook* m_notebook; - wxCheckBox* m_fullscreen; - wxCheckBox* m_separate; - wxCheckBox* m_update; - wxCheckBox* m_dont_show; - wxStaticBoxSizer* m_mlc_box_sizer; - wxStaticText* m_prev_mlc_warning; - wxDirPickerCtrl* m_mlc_folder; - wxDirPickerCtrl* m_game_path; + struct + { + // header + wxStaticText* staticText11{}; + wxStaticText* portableModeInfoText{}; - bool m_game_path_changed = false; - bool m_mlc_changed = false; + // game path box + wxStaticBoxSizer* gamePathBoxSizer{}; + wxStaticText* gamePathText{}; + wxStaticText* gamePathText2{}; + wxDirPickerCtrl* gamePathPicker{}; + }m_page1; + + struct + { + wxCheckBox* fullscreenCheckbox; + wxCheckBox* separateCheckbox; + wxCheckBox* updateCheckbox; + }m_page2; }; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 7a4f3174..c83ab16b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -149,8 +149,6 @@ enum // help MAINFRAME_MENU_ID_HELP_ABOUT = 21700, MAINFRAME_MENU_ID_HELP_UPDATE, - MAINFRAME_MENU_ID_HELP_GETTING_STARTED, - // custom MAINFRAME_ID_TIMER1 = 21800, }; @@ -225,7 +223,6 @@ EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_TEXTURE_RELATIONS, MainWindow::OnDebugView // help menu EVT_MENU(MAINFRAME_MENU_ID_HELP_ABOUT, MainWindow::OnHelpAbout) EVT_MENU(MAINFRAME_MENU_ID_HELP_UPDATE, MainWindow::OnHelpUpdate) -EVT_MENU(MAINFRAME_MENU_ID_HELP_GETTING_STARTED, MainWindow::OnHelpGettingStarted) // misc EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAMELIST_REFRESH, MainWindow::OnRequestGameListRefresh) @@ -418,25 +415,6 @@ wxString MainWindow::GetInitialWindowTitle() return BUILD_VERSION_WITH_NAME_STRING; } -void MainWindow::ShowGettingStartedDialog() -{ - GettingStartedDialog dia(this); - dia.ShowModal(); - if (dia.HasGamePathChanged() || dia.HasMLCChanged()) - m_game_list->ReloadGameEntries(); - - TogglePadView(); - - auto& config = GetConfig(); - m_padViewMenuItem->Check(config.pad_open.GetValue()); - m_fullscreenMenuItem->Check(config.fullscreen.GetValue()); -} - -namespace coreinit -{ - void OSSchedulerEnd(); -}; - void MainWindow::OnClose(wxCloseEvent& event) { wxTheClipboard->Flush(); @@ -2075,11 +2053,6 @@ void MainWindow::OnHelpUpdate(wxCommandEvent& event) test.ShowModal(); } -void MainWindow::OnHelpGettingStarted(wxCommandEvent& event) -{ - ShowGettingStartedDialog(); -} - void MainWindow::RecreateMenu() { if (m_menuBar) @@ -2303,8 +2276,7 @@ void MainWindow::RecreateMenu() if (!std::getenv("APPIMAGE")) { m_check_update_menu->Enable(false); } -#endif - helpMenu->Append(MAINFRAME_MENU_ID_HELP_GETTING_STARTED, _("&Getting started")); +#endif helpMenu->AppendSeparator(); helpMenu->Append(MAINFRAME_MENU_ID_HELP_ABOUT, _("&About Cemu")); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index dd4d0d0d..beb86f98 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -103,7 +103,6 @@ public: void OnAccountSelect(wxCommandEvent& event); void OnConsoleLanguage(wxCommandEvent& event); void OnHelpAbout(wxCommandEvent& event); - void OnHelpGettingStarted(wxCommandEvent& event); void OnHelpUpdate(wxCommandEvent& event); void OnDebugSetting(wxCommandEvent& event); void OnDebugLoggingToggleFlagGeneric(wxCommandEvent& event); @@ -150,7 +149,6 @@ private: void RecreateMenu(); void UpdateChildWindowTitleRunningState(); static wxString GetInitialWindowTitle(); - void ShowGettingStartedDialog(); bool InstallUpdate(const fs::path& metaFilePath); diff --git a/src/main.cpp b/src/main.cpp index 1ccc2805..ea1df684 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,6 @@ #include "Cafe/OS/RPL/rpl.h" #include "Cafe/OS/libs/gx2/GX2.h" #include "Cafe/OS/libs/coreinit/coreinit_Thread.h" -#include "Cafe/HW/Latte/Core/LatteOverlay.h" #include "Cafe/GameProfile/GameProfile.h" #include "Cafe/GraphicPack/GraphicPack2.h" #include "config/CemuConfig.h" @@ -160,7 +159,7 @@ void ExpressionParser_test(); void FSTVolumeTest(); void CRCTest(); -void unitTests() +void UnitTests() { ExpressionParser_test(); gx2CopySurfaceTest(); @@ -169,17 +168,6 @@ void unitTests() CRCTest(); } -int mainEmulatorHLE() -{ - LatteOverlay_init(); - // run a couple of tests if in non-release mode -#ifdef CEMU_DEBUG_ASSERT - unitTests(); -#endif - CemuCommonInit(); - return 0; -} - bool isConsoleConnected = false; void requireConsole() { From a1c1a608d77e6c1f7989127d472c655d3159df62 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Tue, 23 Jul 2024 02:18:48 +0100 Subject: [PATCH 33/73] nsyshid: Emulate Infinity Base (#1246) --- src/Cafe/CMakeLists.txt | 2 + src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp | 8 + src/Cafe/OS/libs/nsyshid/Infinity.cpp | 1102 +++++++++++++++++ src/Cafe/OS/libs/nsyshid/Infinity.h | 105 ++ src/Cafe/OS/libs/nsyshid/Skylander.cpp | 2 +- src/Cafe/OS/libs/nsyshid/Skylander.h | 8 +- src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 1 + .../EmulatedUSBDeviceFrame.cpp | 252 +++- .../EmulatedUSBDeviceFrame.h | 18 + 10 files changed, 1478 insertions(+), 22 deletions(-) create mode 100644 src/Cafe/OS/libs/nsyshid/Infinity.cpp create mode 100644 src/Cafe/OS/libs/nsyshid/Infinity.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 1583bdd7..0fb7a44b 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -463,6 +463,8 @@ add_library(CemuCafe OS/libs/nsyshid/BackendLibusb.h OS/libs/nsyshid/BackendWindowsHID.cpp OS/libs/nsyshid/BackendWindowsHID.h + OS/libs/nsyshid/Infinity.cpp + OS/libs/nsyshid/Infinity.h OS/libs/nsyshid/Skylander.cpp OS/libs/nsyshid/Skylander.h OS/libs/nsyskbd/nsyskbd.cpp diff --git a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp index 11a299ed..95eaf06a 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp @@ -1,4 +1,5 @@ #include "BackendEmulated.h" +#include "Infinity.h" #include "Skylander.h" #include "config/CemuConfig.h" @@ -25,5 +26,12 @@ namespace nsyshid::backend::emulated auto device = std::make_shared(); AttachDevice(device); } + if (GetConfig().emulated_usb_devices.emulate_infinity_base && !FindDeviceById(0x0E6F, 0x0129)) + { + cemuLog_logDebug(LogType::Force, "Attaching Emulated Base"); + // Add Infinity Base + 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/Infinity.cpp b/src/Cafe/OS/libs/nsyshid/Infinity.cpp new file mode 100644 index 00000000..ab44ef4a --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Infinity.cpp @@ -0,0 +1,1102 @@ +#include "Infinity.h" + +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "util/crypto/aes128.h" + +#include +#include "openssl/sha.h" + +namespace nsyshid +{ + static constexpr std::array SHA1_CONSTANT = { + 0xAF, 0x62, 0xD2, 0xEC, 0x04, 0x91, 0x96, 0x8C, 0xC5, 0x2A, 0x1A, 0x71, 0x65, 0xF8, 0x65, 0xFE, + 0x28, 0x63, 0x29, 0x20, 0x44, 0x69, 0x73, 0x6e, 0x65, 0x79, 0x20, 0x32, 0x30, 0x31, 0x33}; + + InfinityUSB g_infinitybase; + + const std::map> s_listFigures = { + {0x0F4241, {1, "Mr. Incredible"}}, + {0x0F4242, {1, "Sulley"}}, + {0x0F4243, {1, "Jack Sparrow"}}, + {0x0F4244, {1, "Lone Ranger"}}, + {0x0F4245, {1, "Tonto"}}, + {0x0F4246, {1, "Lightning McQueen"}}, + {0x0F4247, {1, "Holley Shiftwell"}}, + {0x0F4248, {1, "Buzz Lightyear"}}, + {0x0F4249, {1, "Jessie"}}, + {0x0F424A, {1, "Mike"}}, + {0x0F424B, {1, "Mrs. Incredible"}}, + {0x0F424C, {1, "Hector Barbossa"}}, + {0x0F424D, {1, "Davy Jones"}}, + {0x0F424E, {1, "Randy"}}, + {0x0F424F, {1, "Syndrome"}}, + {0x0F4250, {1, "Woody"}}, + {0x0F4251, {1, "Mater"}}, + {0x0F4252, {1, "Dash"}}, + {0x0F4253, {1, "Violet"}}, + {0x0F4254, {1, "Francesco Bernoulli"}}, + {0x0F4255, {1, "Sorcerer's Apprentice Mickey"}}, + {0x0F4256, {1, "Jack Skellington"}}, + {0x0F4257, {1, "Rapunzel"}}, + {0x0F4258, {1, "Anna"}}, + {0x0F4259, {1, "Elsa"}}, + {0x0F425A, {1, "Phineas"}}, + {0x0F425B, {1, "Agent P"}}, + {0x0F425C, {1, "Wreck-It Ralph"}}, + {0x0F425D, {1, "Vanellope"}}, + {0x0F425E, {1, "Mr. Incredible (Crystal)"}}, + {0x0F425F, {1, "Jack Sparrow (Crystal)"}}, + {0x0F4260, {1, "Sulley (Crystal)"}}, + {0x0F4261, {1, "Lightning McQueen (Crystal)"}}, + {0x0F4262, {1, "Lone Ranger (Crystal)"}}, + {0x0F4263, {1, "Buzz Lightyear (Crystal)"}}, + {0x0F4264, {1, "Agent P (Crystal)"}}, + {0x0F4265, {1, "Sorcerer's Apprentice Mickey (Crystal)"}}, + {0x0F4266, {1, "Buzz Lightyear (Glowing)"}}, + {0x0F42A4, {2, "Captain America"}}, + {0x0F42A5, {2, "Hulk"}}, + {0x0F42A6, {2, "Iron Man"}}, + {0x0F42A7, {2, "Thor"}}, + {0x0F42A8, {2, "Groot"}}, + {0x0F42A9, {2, "Rocket Raccoon"}}, + {0x0F42AA, {2, "Star-Lord"}}, + {0x0F42AB, {2, "Spider-Man"}}, + {0x0F42AC, {2, "Nick Fury"}}, + {0x0F42AD, {2, "Black Widow"}}, + {0x0F42AE, {2, "Hawkeye"}}, + {0x0F42AF, {2, "Drax"}}, + {0x0F42B0, {2, "Gamora"}}, + {0x0F42B1, {2, "Iron Fist"}}, + {0x0F42B2, {2, "Nova"}}, + {0x0F42B3, {2, "Venom"}}, + {0x0F42B4, {2, "Donald Duck"}}, + {0x0F42B5, {2, "Aladdin"}}, + {0x0F42B6, {2, "Stitch"}}, + {0x0F42B7, {2, "Merida"}}, + {0x0F42B8, {2, "Tinker Bell"}}, + {0x0F42B9, {2, "Maleficent"}}, + {0x0F42BA, {2, "Hiro"}}, + {0x0F42BB, {2, "Baymax"}}, + {0x0F42BC, {2, "Loki"}}, + {0x0F42BD, {2, "Ronan"}}, + {0x0F42BE, {2, "Green Goblin"}}, + {0x0F42BF, {2, "Falcon"}}, + {0x0F42C0, {2, "Yondu"}}, + {0x0F42C1, {2, "Jasmine"}}, + {0x0F42C6, {2, "Black Suit Spider-Man"}}, + {0x0F42D6, {3, "Sam Flynn"}}, + {0x0F42D7, {3, "Quorra"}}, + {0x0F4308, {3, "Anakin Skywalker"}}, + {0x0F4309, {3, "Obi-Wan Kenobi"}}, + {0x0F430A, {3, "Yoda"}}, + {0x0F430B, {3, "Ahsoka Tano"}}, + {0x0F430C, {3, "Darth Maul"}}, + {0x0F430E, {3, "Luke Skywalker"}}, + {0x0F430F, {3, "Han Solo"}}, + {0x0F4310, {3, "Princess Leia"}}, + {0x0F4311, {3, "Chewbacca"}}, + {0x0F4312, {3, "Darth Vader"}}, + {0x0F4313, {3, "Boba Fett"}}, + {0x0F4314, {3, "Ezra Bridger"}}, + {0x0F4315, {3, "Kanan Jarrus"}}, + {0x0F4316, {3, "Sabine Wren"}}, + {0x0F4317, {3, "Zeb Orrelios"}}, + {0x0F4318, {3, "Joy"}}, + {0x0F4319, {3, "Anger"}}, + {0x0F431A, {3, "Fear"}}, + {0x0F431B, {3, "Sadness"}}, + {0x0F431C, {3, "Disgust"}}, + {0x0F431D, {3, "Mickey Mouse"}}, + {0x0F431E, {3, "Minnie Mouse"}}, + {0x0F431F, {3, "Mulan"}}, + {0x0F4320, {3, "Olaf"}}, + {0x0F4321, {3, "Vision"}}, + {0x0F4322, {3, "Ultron"}}, + {0x0F4323, {3, "Ant-Man"}}, + {0x0F4325, {3, "Captain America - The First Avenger"}}, + {0x0F4326, {3, "Finn"}}, + {0x0F4327, {3, "Kylo Ren"}}, + {0x0F4328, {3, "Poe Dameron"}}, + {0x0F4329, {3, "Rey"}}, + {0x0F432B, {3, "Spot"}}, + {0x0F432C, {3, "Nick Wilde"}}, + {0x0F432D, {3, "Judy Hopps"}}, + {0x0F432E, {3, "Hulkbuster"}}, + {0x0F432F, {3, "Anakin Skywalker (Light FX)"}}, + {0x0F4330, {3, "Obi-Wan Kenobi (Light FX)"}}, + {0x0F4331, {3, "Yoda (Light FX)"}}, + {0x0F4332, {3, "Luke Skywalker (Light FX)"}}, + {0x0F4333, {3, "Darth Vader (Light FX)"}}, + {0x0F4334, {3, "Kanan Jarrus (Light FX)"}}, + {0x0F4335, {3, "Kylo Ren (Light FX)"}}, + {0x0F4336, {3, "Black Panther"}}, + {0x0F436C, {3, "Nemo"}}, + {0x0F436D, {3, "Dory"}}, + {0x0F436E, {3, "Baloo"}}, + {0x0F436F, {3, "Alice"}}, + {0x0F4370, {3, "Mad Hatter"}}, + {0x0F4371, {3, "Time"}}, + {0x0F4372, {3, "Peter Pan"}}, + {0x1E8481, {1, "Starter Play Set"}}, + {0x1E8482, {1, "Lone Ranger Play Set"}}, + {0x1E8483, {1, "Cars Play Set"}}, + {0x1E8484, {1, "Toy Story in Space Play Set"}}, + {0x1E84E4, {2, "Marvel's The Avengers Play Set"}}, + {0x1E84E5, {2, "Marvel's Spider-Man Play Set"}}, + {0x1E84E6, {2, "Marvel's Guardians of the Galaxy Play Set"}}, + {0x1E84E7, {2, "Assault on Asgard"}}, + {0x1E84E8, {2, "Escape from the Kyln"}}, + {0x1E84E9, {2, "Stitch's Tropical Rescue"}}, + {0x1E84EA, {2, "Brave Forest Siege"}}, + {0x1E8548, {3, "Inside Out Play Set"}}, + {0x1E8549, {3, "Star Wars: Twilight of the Republic Play Set"}}, + {0x1E854A, {3, "Star Wars: Rise Against the Empire Play Set"}}, + {0x1E854B, {3, "Star Wars: The Force Awakens Play Set"}}, + {0x1E854C, {3, "Marvel Battlegrounds Play Set"}}, + {0x1E854D, {3, "Toy Box Speedway"}}, + {0x1E854E, {3, "Toy Box Takeover"}}, + {0x1E85AC, {3, "Finding Dory Play Set"}}, + {0x2DC6C3, {1, "Bolt's Super Strength"}}, + {0x2DC6C4, {1, "Ralph's Power of Destruction"}}, + {0x2DC6C5, {1, "Chernabog's Power"}}, + {0x2DC6C6, {1, "C.H.R.O.M.E. Damage Increaser"}}, + {0x2DC6C7, {1, "Dr. Doofenshmirtz's Damage-Inator!"}}, + {0x2DC6C8, {1, "Electro-Charge"}}, + {0x2DC6C9, {1, "Fix-It Felix's Repair Power"}}, + {0x2DC6CA, {1, "Rapunzel's Healing"}}, + {0x2DC6CB, {1, "C.H.R.O.M.E. Armor Shield"}}, + {0x2DC6CC, {1, "Star Command Shield"}}, + {0x2DC6CD, {1, "Violet's Force Field"}}, + {0x2DC6CE, {1, "Pieces of Eight"}}, + {0x2DC6CF, {1, "Scrooge McDuck's Lucky Dime"}}, + {0x2DC6D0, {1, "User Control"}}, + {0x2DC6D1, {1, "Sorcerer Mickey's Hat"}}, + {0x2DC6FE, {1, "Emperor Zurg's Wrath"}}, + {0x2DC6FF, {1, "Merlin's Summon"}}, + {0x2DC765, {2, "Enchanted Rose"}}, + {0x2DC766, {2, "Mulan's Training Uniform"}}, + {0x2DC767, {2, "Flubber"}}, + {0x2DC768, {2, "S.H.I.E.L.D. Helicarrier Strike"}}, + {0x2DC769, {2, "Zeus' Thunderbolts"}}, + {0x2DC76A, {2, "King Louie's Monkeys"}}, + {0x2DC76B, {2, "Infinity Gauntlet"}}, + {0x2DC76D, {2, "Sorcerer Supreme"}}, + {0x2DC76E, {2, "Maleficent's Spell Cast"}}, + {0x2DC76F, {2, "Chernabog's Spirit Cyclone"}}, + {0x2DC770, {2, "Marvel Team-Up: Capt. Marvel"}}, + {0x2DC771, {2, "Marvel Team-Up: Iron Patriot"}}, + {0x2DC772, {2, "Marvel Team-Up: Ant-Man"}}, + {0x2DC773, {2, "Marvel Team-Up: White Tiger"}}, + {0x2DC774, {2, "Marvel Team-Up: Yondu"}}, + {0x2DC775, {2, "Marvel Team-Up: Winter Soldier"}}, + {0x2DC776, {2, "Stark Arc Reactor"}}, + {0x2DC777, {2, "Gamma Rays"}}, + {0x2DC778, {2, "Alien Symbiote"}}, + {0x2DC779, {2, "All for One"}}, + {0x2DC77A, {2, "Sandy Claws Surprise"}}, + {0x2DC77B, {2, "Glory Days"}}, + {0x2DC77C, {2, "Cursed Pirate Gold"}}, + {0x2DC77D, {2, "Sentinel of Liberty"}}, + {0x2DC77E, {2, "The Immortal Iron Fist"}}, + {0x2DC77F, {2, "Space Armor"}}, + {0x2DC780, {2, "Rags to Riches"}}, + {0x2DC781, {2, "Ultimate Falcon"}}, + {0x2DC788, {3, "Tomorrowland Time Bomb"}}, + {0x2DC78E, {3, "Galactic Team-Up: Mace Windu"}}, + {0x2DC791, {3, "Luke's Rebel Alliance Flight Suit Costume"}}, + {0x2DC798, {3, "Finn's Stormtrooper Costume"}}, + {0x2DC799, {3, "Poe's Resistance Jacket"}}, + {0x2DC79A, {3, "Resistance Tactical Strike"}}, + {0x2DC79E, {3, "Officer Nick Wilde"}}, + {0x2DC79F, {3, "Meter Maid Judy"}}, + {0x2DC7A2, {3, "Darkhawk's Blast"}}, + {0x2DC7A3, {3, "Cosmic Cube Blast"}}, + {0x2DC7A4, {3, "Princess Leia's Boushh Disguise"}}, + {0x2DC7A6, {3, "Nova Corps Strike"}}, + {0x2DC7A7, {3, "King Mickey"}}, + {0x3D0912, {1, "Mickey's Car"}}, + {0x3D0913, {1, "Cinderella's Coach"}}, + {0x3D0914, {1, "Electric Mayhem Bus"}}, + {0x3D0915, {1, "Cruella De Vil's Car"}}, + {0x3D0916, {1, "Pizza Planet Delivery Truck"}}, + {0x3D0917, {1, "Mike's New Car"}}, + {0x3D0919, {1, "Parking Lot Tram"}}, + {0x3D091A, {1, "Captain Hook's Ship"}}, + {0x3D091B, {1, "Dumbo"}}, + {0x3D091C, {1, "Calico Helicopter"}}, + {0x3D091D, {1, "Maximus"}}, + {0x3D091E, {1, "Angus"}}, + {0x3D091F, {1, "Abu the Elephant"}}, + {0x3D0920, {1, "Headless Horseman's Horse"}}, + {0x3D0921, {1, "Phillipe"}}, + {0x3D0922, {1, "Khan"}}, + {0x3D0923, {1, "Tantor"}}, + {0x3D0924, {1, "Dragon Firework Cannon"}}, + {0x3D0925, {1, "Stitch's Blaster"}}, + {0x3D0926, {1, "Toy Story Mania Blaster"}}, + {0x3D0927, {1, "Flamingo Croquet Mallet"}}, + {0x3D0928, {1, "Carl Fredricksen's Cane"}}, + {0x3D0929, {1, "Hangin' Ten Stitch With Surfboard"}}, + {0x3D092A, {1, "Condorman Glider"}}, + {0x3D092B, {1, "WALL-E's Fire Extinguisher"}}, + {0x3D092C, {1, "On the Grid"}}, + {0x3D092D, {1, "WALL-E's Collection"}}, + {0x3D092E, {1, "King Candy's Dessert Toppings"}}, + {0x3D0930, {1, "Victor's Experiments"}}, + {0x3D0931, {1, "Jack's Scary Decorations"}}, + {0x3D0933, {1, "Frozen Flourish"}}, + {0x3D0934, {1, "Rapunzel's Kingdom"}}, + {0x3D0935, {1, "TRON Interface"}}, + {0x3D0936, {1, "Buy N Large Atmosphere"}}, + {0x3D0937, {1, "Sugar Rush Sky"}}, + {0x3D0939, {1, "New Holland Skyline"}}, + {0x3D093A, {1, "Halloween Town Sky"}}, + {0x3D093C, {1, "Chill in the Air"}}, + {0x3D093D, {1, "Rapunzel's Birthday Sky"}}, + {0x3D0940, {1, "Astro Blasters Space Cruiser"}}, + {0x3D0941, {1, "Marlin's Reef"}}, + {0x3D0942, {1, "Nemo's Seascape"}}, + {0x3D0943, {1, "Alice's Wonderland"}}, + {0x3D0944, {1, "Tulgey Wood"}}, + {0x3D0945, {1, "Tri-State Area Terrain"}}, + {0x3D0946, {1, "Danville Sky"}}, + {0x3D0965, {2, "Stark Tech"}}, + {0x3D0966, {2, "Spider-Streets"}}, + {0x3D0967, {2, "World War Hulk"}}, + {0x3D0968, {2, "Gravity Falls Forest"}}, + {0x3D0969, {2, "Neverland"}}, + {0x3D096A, {2, "Simba's Pridelands"}}, + {0x3D096C, {2, "Calhoun's Command"}}, + {0x3D096D, {2, "Star-Lord's Galaxy"}}, + {0x3D096E, {2, "Dinosaur World"}}, + {0x3D096F, {2, "Groot's Roots"}}, + {0x3D0970, {2, "Mulan's Countryside"}}, + {0x3D0971, {2, "The Sands of Agrabah"}}, + {0x3D0974, {2, "A Small World"}}, + {0x3D0975, {2, "View from the Suit"}}, + {0x3D0976, {2, "Spider-Sky"}}, + {0x3D0977, {2, "World War Hulk Sky"}}, + {0x3D0978, {2, "Gravity Falls Sky"}}, + {0x3D0979, {2, "Second Star to the Right"}}, + {0x3D097A, {2, "The King's Domain"}}, + {0x3D097C, {2, "CyBug Swarm"}}, + {0x3D097D, {2, "The Rip"}}, + {0x3D097E, {2, "Forgotten Skies"}}, + {0x3D097F, {2, "Groot's View"}}, + {0x3D0980, {2, "The Middle Kingdom"}}, + {0x3D0984, {2, "Skies of the World"}}, + {0x3D0985, {2, "S.H.I.E.L.D. Containment Truck"}}, + {0x3D0986, {2, "Main Street Electrical Parade Float"}}, + {0x3D0987, {2, "Mr. Toad's Motorcar"}}, + {0x3D0988, {2, "Le Maximum"}}, + {0x3D0989, {2, "Alice in Wonderland's Caterpillar"}}, + {0x3D098A, {2, "Eglantine's Motorcycle"}}, + {0x3D098B, {2, "Medusa's Swamp Mobile"}}, + {0x3D098C, {2, "Hydra Motorcycle"}}, + {0x3D098D, {2, "Darkwing Duck's Ratcatcher"}}, + {0x3D098F, {2, "The USS Swinetrek"}}, + {0x3D0991, {2, "Spider-Copter"}}, + {0x3D0992, {2, "Aerial Area Rug"}}, + {0x3D0993, {2, "Jack-O-Lantern's Glider"}}, + {0x3D0994, {2, "Spider-Buggy"}}, + {0x3D0995, {2, "Jack Skellington's Reindeer"}}, + {0x3D0996, {2, "Fantasyland Carousel Horse"}}, + {0x3D0997, {2, "Odin's Horse"}}, + {0x3D0998, {2, "Gus the Mule"}}, + {0x3D099A, {2, "Darkwing Duck's Grappling Gun"}}, + {0x3D099C, {2, "Ghost Rider's Chain Whip"}}, + {0x3D099D, {2, "Lew Zealand's Boomerang Fish"}}, + {0x3D099E, {2, "Sergeant Calhoun's Blaster"}}, + {0x3D09A0, {2, "Falcon's Wings"}}, + {0x3D09A1, {2, "Mabel's Kittens for Fists"}}, + {0x3D09A2, {2, "Jim Hawkins' Solar Board"}}, + {0x3D09A3, {2, "Black Panther's Vibranium Knives"}}, + {0x3D09A4, {2, "Cloak of Levitation"}}, + {0x3D09A5, {2, "Aladdin's Magic Carpet"}}, + {0x3D09A6, {2, "Honey Lemon's Ice Capsules"}}, + {0x3D09A7, {2, "Jasmine's Palace View"}}, + {0x3D09C1, {2, "Lola"}}, + {0x3D09C2, {2, "Spider-Cycle"}}, + {0x3D09C3, {2, "The Avenjet"}}, + {0x3D09C4, {2, "Spider-Glider"}}, + {0x3D09C5, {2, "Light Cycle"}}, + {0x3D09C6, {2, "Light Jet"}}, + {0x3D09C9, {3, "Retro Ray Gun"}}, + {0x3D09CA, {3, "Tomorrowland Futurescape"}}, + {0x3D09CB, {3, "Tomorrowland Stratosphere"}}, + {0x3D09CC, {3, "Skies Over Felucia"}}, + {0x3D09CD, {3, "Forests of Felucia"}}, + {0x3D09CF, {3, "General Grievous' Wheel Bike"}}, + {0x3D09D2, {3, "Slave I Flyer"}}, + {0x3D09D3, {3, "Y-Wing Fighter"}}, + {0x3D09D4, {3, "Arlo"}}, + {0x3D09D5, {3, "Nash"}}, + {0x3D09D6, {3, "Butch"}}, + {0x3D09D7, {3, "Ramsey"}}, + {0x3D09DC, {3, "Stars Over Sahara Square"}}, + {0x3D09DD, {3, "Sahara Square Sands"}}, + {0x3D09E0, {3, "Ghost Rider's Motorcycle"}}, + {0x3D09E5, {3, "Quad Jumper"}}}; + + InfinityBaseDevice::InfinityBaseDevice() + : Device(0x0E6F, 0x0129, 1, 2, 0) + { + m_IsOpened = false; + } + + bool InfinityBaseDevice::Open() + { + if (!IsOpened()) + { + m_IsOpened = true; + } + return true; + } + + void InfinityBaseDevice::Close() + { + if (IsOpened()) + { + m_IsOpened = false; + } + } + + bool InfinityBaseDevice::IsOpened() + { + return m_IsOpened; + } + + Device::ReadResult InfinityBaseDevice::Read(ReadMessage* message) + { + memcpy(message->data, g_infinitybase.GetStatus().data(), message->length); + message->bytesRead = message->length; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return Device::ReadResult::Success; + } + + Device::WriteResult InfinityBaseDevice::Write(WriteMessage* message) + { + g_infinitybase.SendCommand(message->data, message->length); + message->bytesWritten = message->length; + return Device::WriteResult::Success; + } + + bool InfinityBaseDevice::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 InfinityBaseDevice::SetProtocol(uint8 ifIndex, uint8 protocol) + { + return true; + } + + bool InfinityBaseDevice::SetReport(ReportMessage* message) + { + return true; + } + + std::array InfinityUSB::GetStatus() + { + std::array response = {}; + + bool responded = false; + + do + { + if (!m_figureAddedRemovedResponses.empty()) + { + memcpy(response.data(), m_figureAddedRemovedResponses.front().data(), + 0x20); + m_figureAddedRemovedResponses.pop(); + responded = true; + } + else if (!m_queries.empty()) + { + memcpy(response.data(), m_queries.front().data(), 0x20); + m_queries.pop(); + responded = true; + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + /* code */ + } + while (!responded); + + return response; + } + + void InfinityUSB::SendCommand(uint8* buf, sint32 originalLength) + { + const uint8 command = buf[2]; + const uint8 sequence = buf[3]; + + std::array q_result{}; + + switch (command) + { + case 0x80: + { + q_result = {0xaa, 0x15, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x03, + 0x02, 0x09, 0x09, 0x43, 0x20, 0x32, 0x62, 0x36, + 0x36, 0x4b, 0x34, 0x99, 0x67, 0x31, 0x93, 0x8c}; + break; + } + case 0x81: + { + // Initiate Challenge + g_infinitybase.DescrambleAndSeed(buf, sequence, q_result); + break; + } + case 0x83: + { + // Challenge Response + g_infinitybase.GetNextAndScramble(sequence, q_result); + break; + } + case 0x90: + case 0x92: + case 0x93: + case 0x95: + case 0x96: + { + // Color commands + g_infinitybase.GetBlankResponse(sequence, q_result); + break; + } + case 0xA1: + { + // Get Present Figures + g_infinitybase.GetPresentFigures(sequence, q_result); + break; + } + case 0xA2: + { + // Read Block from Figure + g_infinitybase.QueryBlock(buf[4], buf[5], q_result, sequence); + break; + } + case 0xA3: + { + // Write block to figure + g_infinitybase.WriteBlock(buf[4], buf[5], &buf[7], q_result, sequence); + break; + } + case 0xB4: + { + // Get figure ID + g_infinitybase.GetFigureIdentifier(buf[4], sequence, q_result); + break; + } + case 0xB5: + { + // Get status? + g_infinitybase.GetBlankResponse(sequence, q_result); + break; + } + default: + cemu_assert_error(); + break; + } + + m_queries.push(q_result); + } + + uint8 InfinityUSB::GenerateChecksum(const std::array& data, + int numOfBytes) const + { + int checksum = 0; + for (int i = 0; i < numOfBytes; i++) + { + checksum += data[i]; + } + return (checksum & 0xFF); + } + + void InfinityUSB::GetBlankResponse(uint8 sequence, + std::array& replyBuf) + { + replyBuf[0] = 0xaa; + replyBuf[1] = 0x01; + replyBuf[2] = sequence; + replyBuf[3] = GenerateChecksum(replyBuf, 3); + } + + void InfinityUSB::DescrambleAndSeed(uint8* buf, uint8 sequence, + std::array& replyBuf) + { + uint64 value = uint64(buf[4]) << 56 | uint64(buf[5]) << 48 | + uint64(buf[6]) << 40 | uint64(buf[7]) << 32 | + uint64(buf[8]) << 24 | uint64(buf[9]) << 16 | + uint64(buf[10]) << 8 | uint64(buf[11]); + uint32 seed = Descramble(value); + GenerateSeed(seed); + GetBlankResponse(sequence, replyBuf); + } + + void InfinityUSB::GetNextAndScramble(uint8 sequence, + std::array& replyBuf) + { + const uint32 nextRandom = GetNext(); + const uint64 scrambledNextRandom = Scramble(nextRandom, 0); + replyBuf = {0xAA, 0x09, sequence}; + replyBuf[3] = uint8((scrambledNextRandom >> 56) & 0xFF); + replyBuf[4] = uint8((scrambledNextRandom >> 48) & 0xFF); + replyBuf[5] = uint8((scrambledNextRandom >> 40) & 0xFF); + replyBuf[6] = uint8((scrambledNextRandom >> 32) & 0xFF); + replyBuf[7] = uint8((scrambledNextRandom >> 24) & 0xFF); + replyBuf[8] = uint8((scrambledNextRandom >> 16) & 0xFF); + replyBuf[9] = uint8((scrambledNextRandom >> 8) & 0xFF); + replyBuf[10] = uint8(scrambledNextRandom & 0xFF); + replyBuf[11] = GenerateChecksum(replyBuf, 11); + } + + uint32 InfinityUSB::Descramble(uint64 numToDescramble) + { + uint64 mask = 0x8E55AA1B3999E8AA; + uint32 ret = 0; + + for (int i = 0; i < 64; i++) + { + if (mask & 0x8000000000000000) + { + ret = (ret << 1) | (numToDescramble & 0x01); + } + + numToDescramble >>= 1; + mask <<= 1; + } + + return ret; + } + + uint64 InfinityUSB::Scramble(uint32 numToScramble, uint32 garbage) + { + uint64 mask = 0x8E55AA1B3999E8AA; + uint64 ret = 0; + + for (int i = 0; i < 64; i++) + { + ret <<= 1; + + if ((mask & 1) != 0) + { + ret |= (numToScramble & 1); + numToScramble >>= 1; + } + else + { + ret |= (garbage & 1); + garbage >>= 1; + } + + mask >>= 1; + } + + return ret; + } + + void InfinityUSB::GenerateSeed(uint32 seed) + { + m_randomA = 0xF1EA5EED; + m_randomB = seed; + m_randomC = seed; + m_randomD = seed; + + for (int i = 0; i < 23; i++) + { + GetNext(); + } + } + + uint32 InfinityUSB::GetNext() + { + uint32 a = m_randomA; + uint32 b = m_randomB; + uint32 c = m_randomC; + uint32 ret = std::rotl(m_randomB, 27); + + const uint32 temp = (a + ((ret ^ 0xFFFFFFFF) + 1)); + b ^= std::rotl(c, 17); + a = m_randomD; + c += a; + ret = b + temp; + a += temp; + + m_randomC = a; + m_randomA = b; + m_randomB = c; + m_randomD = ret; + + return ret; + } + + void InfinityUSB::GetPresentFigures(uint8 sequence, + std::array& replyBuf) + { + int x = 3; + for (uint8 i = 0; i < m_figures.size(); i++) + { + uint8 slot = i == 0 ? 0x10 : (i < 4) ? 0x20 + : 0x30; + if (m_figures[i].present) + { + replyBuf[x] = slot + m_figures[i].orderAdded; + replyBuf[x + 1] = 0x09; + x += 2; + } + } + replyBuf[0] = 0xaa; + replyBuf[1] = x - 2; + replyBuf[2] = sequence; + replyBuf[x] = GenerateChecksum(replyBuf, x); + } + + InfinityUSB::InfinityFigure& + InfinityUSB::GetFigureByOrder(uint8 orderAdded) + { + for (uint8 i = 0; i < m_figures.size(); i++) + { + if (m_figures[i].orderAdded == orderAdded) + { + return m_figures[i]; + } + } + return m_figures[0]; + } + + void InfinityUSB::QueryBlock(uint8 fig_num, uint8 block, + std::array& replyBuf, + uint8 sequence) + { + std::lock_guard lock(m_infinityMutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + replyBuf[0] = 0xaa; + replyBuf[1] = 0x12; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + const uint8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) + { + memcpy(&replyBuf[4], figure.data.data() + (16 * file_block), 16); + } + replyBuf[20] = GenerateChecksum(replyBuf, 20); + } + + void InfinityUSB::WriteBlock(uint8 fig_num, uint8 block, + const uint8* to_write_buf, + std::array& replyBuf, + uint8 sequence) + { + std::lock_guard lock(m_infinityMutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + replyBuf[0] = 0xaa; + replyBuf[1] = 0x02; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + const uint8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) + { + memcpy(figure.data.data() + (file_block * 16), to_write_buf, 16); + figure.Save(); + } + replyBuf[4] = GenerateChecksum(replyBuf, 4); + } + + void InfinityUSB::GetFigureIdentifier(uint8 fig_num, uint8 sequence, + std::array& replyBuf) + { + std::lock_guard lock(m_infinityMutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + replyBuf[0] = 0xaa; + replyBuf[1] = 0x09; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + + if (figure.present) + { + memcpy(&replyBuf[4], figure.data.data(), 7); + } + replyBuf[11] = GenerateChecksum(replyBuf, 11); + } + + std::pair InfinityUSB::FindFigure(uint32 figNum) + { + for (const auto& it : GetFigureList()) + { + if (it.first == figNum) + { + return it.second; + } + } + return {0, fmt::format("Unknown Figure ({})", figNum)}; + } + + std::map> InfinityUSB::GetFigureList() + { + return s_listFigures; + } + + void InfinityUSB::InfinityFigure::Save() + { + if (!infFile) + return; + + infFile->SetPosition(0); + infFile->writeData(data.data(), data.size()); + } + + bool InfinityUSB::RemoveFigure(uint8 position) + { + std::lock_guard lock(m_infinityMutex); + InfinityFigure& figure = m_figures[position]; + + figure.Save(); + figure.infFile.reset(); + + if (figure.present) + { + figure.present = false; + + position = DeriveFigurePosition(position); + if (position == 0) + { + return false; + } + + std::array figureChangeResponse = {0xab, 0x04, position, 0x09, figure.orderAdded, + 0x01}; + figureChangeResponse[6] = GenerateChecksum(figureChangeResponse, 6); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return true; + } + return false; + } + + uint32 + InfinityUSB::LoadFigure(const std::array& buf, + std::unique_ptr inFile, uint8 position) + { + std::lock_guard lock(m_infinityMutex); + uint8 orderAdded; + + std::vector sha1Calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; + for (int i = 0; i < 7; i++) + { + sha1Calc.push_back(buf[i]); + } + + std::array key = GenerateInfinityFigureKey(sha1Calc); + + std::array infinity_decrypted_block = {}; + std::array encryptedBlock = {}; + memcpy(encryptedBlock.data(), &buf[16], 16); + + AES128_ECB_decrypt(encryptedBlock.data(), key.data(), infinity_decrypted_block.data()); + + uint32 number = uint32(infinity_decrypted_block[1]) << 16 | uint32(infinity_decrypted_block[2]) << 8 | + uint32(infinity_decrypted_block[3]); + + InfinityFigure& figure = m_figures[position]; + + figure.infFile = std::move(inFile); + memcpy(figure.data.data(), buf.data(), figure.data.size()); + figure.present = true; + if (figure.orderAdded == 255) + { + figure.orderAdded = m_figureOrder; + m_figureOrder++; + } + orderAdded = figure.orderAdded; + + position = DeriveFigurePosition(position); + if (position == 0) + { + return 0; + } + + std::array figureChangeResponse = {0xab, 0x04, position, 0x09, orderAdded, 0x00}; + figureChangeResponse[6] = GenerateChecksum(figureChangeResponse, 6); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return number; + } + + static uint32 InfinityCRC32(const std::array& buffer) + { + static constexpr std::array CRC32_TABLE{ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d}; + + // Infinity m_figures calculate their CRC32 based on 12 bytes in the block of 16 + uint32 ret = 0; + for (uint32 i = 0; i < 12; ++i) + { + uint8 index = uint8(ret & 0xFF) ^ buffer[i]; + ret = ((ret >> 8) ^ CRC32_TABLE[index]); + } + + return ret; + } + + bool InfinityUSB::CreateFigure(fs::path pathName, uint32 figureNum, uint8 series) + { + FileStream* infFile(FileStream::createFile2(pathName)); + if (!infFile) + { + return false; + } + std::array fileData{}; + uint32 firstBlock = 0x17878E; + uint32 otherBlocks = 0x778788; + for (sint8 i = 2; i >= 0; i--) + { + fileData[0x38 - i] = uint8((firstBlock >> i * 8) & 0xFF); + } + for (uint32 index = 1; index < 0x05; index++) + { + for (sint8 i = 2; i >= 0; i--) + { + fileData[((index * 0x40) + 0x38) - i] = uint8((otherBlocks >> i * 8) & 0xFF); + } + } + // Create the vector to calculate the SHA1 hash with + std::vector sha1Calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; + + // Generate random UID, used for AES encrypt/decrypt + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0, 255); + std::array uid_data = {0, 0, 0, 0, 0, 0, 0, 0x89, 0x44, 0x00, 0xC2}; + uid_data[0] = dist(mt); + uid_data[1] = dist(mt); + uid_data[2] = dist(mt); + uid_data[3] = dist(mt); + uid_data[4] = dist(mt); + uid_data[5] = dist(mt); + uid_data[6] = dist(mt); + for (sint8 i = 0; i < 7; i++) + { + sha1Calc.push_back(uid_data[i]); + } + std::array figureData = GenerateBlankFigureData(figureNum, series); + if (figureData[1] == 0x00) + return false; + + std::array key = GenerateInfinityFigureKey(sha1Calc); + + std::array encryptedBlock = {}; + std::array blankBlock = {}; + std::array encryptedBlank = {}; + + AES128_ECB_encrypt(figureData.data(), key.data(), encryptedBlock.data()); + AES128_ECB_encrypt(blankBlock.data(), key.data(), encryptedBlank.data()); + + memcpy(&fileData[0], uid_data.data(), uid_data.size()); + memcpy(&fileData[16], encryptedBlock.data(), encryptedBlock.size()); + memcpy(&fileData[16 * 0x04], encryptedBlank.data(), encryptedBlank.size()); + memcpy(&fileData[16 * 0x08], encryptedBlank.data(), encryptedBlank.size()); + memcpy(&fileData[16 * 0x0C], encryptedBlank.data(), encryptedBlank.size()); + memcpy(&fileData[16 * 0x0D], encryptedBlank.data(), encryptedBlank.size()); + + infFile->writeData(fileData.data(), fileData.size()); + + delete infFile; + + return true; + } + + 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)); + // 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 = {}; + for (int i = 0; i < 4; i++) + { + for (int x = 3; x >= 0; x--) + { + key[(3 - x) + (i * 4)] = digest[x + (i * 4)]; + } + } + return key; + } + + std::array InfinityUSB::GenerateBlankFigureData(uint32 figureNum, uint8 series) + { + std::array figureData = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xD1, 0x1F}; + + // Figure Number, input by end user + figureData[1] = uint8((figureNum >> 16) & 0xFF); + figureData[2] = uint8((figureNum >> 8) & 0xFF); + figureData[3] = uint8(figureNum & 0xFF); + + // Manufacture date, formatted as YY/MM/DD. Set to release date of figure's series + if (series == 1) + { + figureData[4] = 0x0D; + figureData[5] = 0x08; + figureData[6] = 0x12; + } + else if (series == 2) + { + figureData[4] = 0x0E; + figureData[5] = 0x09; + figureData[6] = 0x12; + } + else if (series == 3) + { + figureData[4] = 0x0F; + figureData[5] = 0x08; + figureData[6] = 0x1C; + } + + uint32 checksum = InfinityCRC32(figureData); + for (sint8 i = 3; i >= 0; i--) + { + figureData[15 - i] = uint8((checksum >> i * 8) & 0xFF); + } + return figureData; + } + + uint8 InfinityUSB::DeriveFigurePosition(uint8 position) + { + // In the added/removed response, position needs to be 1 for the hexagon, 2 for Player 1 and + // Player 1's abilities, and 3 for Player 2 and Player 2's abilities. In the UI, positions 0, 1 + // and 2 represent the hexagon slot, 3, 4 and 5 represent Player 1's slot and 6, 7 and 8 represent + // Player 2's slot. + + switch (position) + { + case 0: + case 1: + case 2: + return 1; + case 3: + case 4: + case 5: + return 2; + case 6: + case 7: + case 8: + return 3; + + default: + return 0; + } + } +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Infinity.h b/src/Cafe/OS/libs/nsyshid/Infinity.h new file mode 100644 index 00000000..aa98fd15 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Infinity.h @@ -0,0 +1,105 @@ +#pragma once + +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +namespace nsyshid +{ + class InfinityBaseDevice final : public Device { + public: + InfinityBaseDevice(); + ~InfinityBaseDevice() = 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; + }; + + constexpr uint16 INF_BLOCK_COUNT = 0x14; + constexpr uint16 INF_BLOCK_SIZE = 0x10; + constexpr uint16 INF_FIGURE_SIZE = INF_BLOCK_COUNT * INF_BLOCK_SIZE; + constexpr uint8 MAX_FIGURES = 9; + class InfinityUSB { + public: + struct InfinityFigure final + { + std::unique_ptr infFile; + std::array data{}; + bool present = false; + uint8 orderAdded = 255; + void Save(); + }; + + void SendCommand(uint8* buf, sint32 originalLength); + std::array GetStatus(); + + void GetBlankResponse(uint8 sequence, std::array& replyBuf); + void DescrambleAndSeed(uint8* buf, uint8 sequence, + std::array& replyBuf); + void GetNextAndScramble(uint8 sequence, std::array& replyBuf); + void GetPresentFigures(uint8 sequence, std::array& replyBuf); + void QueryBlock(uint8 figNum, uint8 block, std::array& replyBuf, + uint8 sequence); + void WriteBlock(uint8 figNum, uint8 block, const uint8* toWriteBuf, + std::array& replyBuf, uint8 sequence); + void GetFigureIdentifier(uint8 figNum, uint8 sequence, + std::array& replyBuf); + + bool RemoveFigure(uint8 position); + uint32 LoadFigure(const std::array& buf, + std::unique_ptr, uint8 position); + bool CreateFigure(fs::path pathName, uint32 figureNum, uint8 series); + static std::map> GetFigureList(); + std::pair FindFigure(uint32 figNum); + + protected: + std::shared_mutex m_infinityMutex; + std::array m_figures; + + private: + uint8 GenerateChecksum(const std::array& data, + int numOfBytes) const; + uint32 Descramble(uint64 numToDescramble); + uint64 Scramble(uint32 numToScramble, uint32 garbage); + void GenerateSeed(uint32 seed); + uint32 GetNext(); + InfinityFigure& GetFigureByOrder(uint8 orderAdded); + uint8 DeriveFigurePosition(uint8 position); + std::array GenerateInfinityFigureKey(const std::vector& sha1Data); + std::array GenerateBlankFigureData(uint32 figureNum, uint8 series); + + uint32 m_randomA; + uint32 m_randomB; + uint32 m_randomC; + uint32 m_randomD; + + uint8 m_figureOrder = 0; + std::queue> m_figureAddedRemovedResponses; + std::queue> m_queries; + }; + extern InfinityUSB g_infinitybase; + +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp index 7f17f8a3..a9888787 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.cpp +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -855,7 +855,7 @@ namespace nsyshid return false; } - std::array data{}; + std::array data{}; uint32 first_block = 0x690F0F0F; uint32 other_blocks = 0x69080F7F; diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h index ae8b5d92..95eaff0c 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.h +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -38,9 +38,9 @@ namespace nsyshid bool m_IsOpened; }; - constexpr uint16 BLOCK_COUNT = 0x40; - constexpr uint16 BLOCK_SIZE = 0x10; - constexpr uint16 FIGURE_SIZE = BLOCK_COUNT * BLOCK_SIZE; + constexpr uint16 SKY_BLOCK_COUNT = 0x40; + constexpr uint16 SKY_BLOCK_SIZE = 0x10; + constexpr uint16 SKY_FIGURE_SIZE = SKY_BLOCK_COUNT * SKY_BLOCK_SIZE; constexpr uint8 MAX_SKYLANDERS = 16; class SkylanderUSB { @@ -50,7 +50,7 @@ namespace nsyshid std::unique_ptr skyFile; uint8 status = 0; std::queue queuedStatus; - std::array data{}; + std::array data{}; uint32 lastId = 0; void Save(); diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 03b12731..338392dd 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -344,6 +344,7 @@ void CemuConfig::Load(XMLConfigParser& parser) // emulatedusbdevices 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); } void CemuConfig::Save(XMLConfigParser& parser) @@ -541,6 +542,7 @@ void CemuConfig::Save(XMLConfigParser& parser) // emulated usb devices 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()); } GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 3f3da953..2a1d29cb 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -519,6 +519,7 @@ struct CemuConfig struct { ConfigValue emulate_skylander_portal{false}; + ConfigValue emulate_infinity_base{true}; }emulated_usb_devices{}; private: diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index f43c3690..f4784f35 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -43,6 +43,7 @@ EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) auto* notebook = new wxNotebook(this, wxID_ANY); notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal")); + notebook->AddPage(AddInfinityPage(notebook), _("Infinity Base")); sizer->Add(notebook, 1, wxEXPAND | wxALL, 2); @@ -83,32 +84,98 @@ wxPanel* EmulatedUSBDeviceFrame::AddSkylanderPage(wxNotebook* notebook) return panel; } -wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 row_number, +wxPanel* EmulatedUSBDeviceFrame::AddInfinityPage(wxNotebook* notebook) +{ + auto* panel = new wxPanel(notebook); + auto* panelSizer = new wxBoxSizer(wxBOTH); + auto* box = new wxStaticBox(panel, wxID_ANY, _("Infinity Manager")); + auto* boxSizer = new wxStaticBoxSizer(box, wxBOTH); + + auto* row = new wxBoxSizer(wxHORIZONTAL); + + m_emulateBase = + new wxCheckBox(box, wxID_ANY, _("Emulate Infinity Base")); + m_emulateBase->SetValue( + GetConfig().emulated_usb_devices.emulate_infinity_base); + m_emulateBase->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + GetConfig().emulated_usb_devices.emulate_infinity_base = + m_emulateBase->IsChecked(); + g_config.Save(); + }); + row->Add(m_emulateBase, 1, wxEXPAND | wxALL, 2); + boxSizer->Add(row, 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Play Set/Power Disc", 0, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Power Disc Two", 1, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Power Disc Three", 2, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player One", 3, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player One Ability One", 4, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player One Ability Two", 5, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player Two", 6, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player Two Ability One", 7, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player Two Ability Two", 8, box), 1, wxEXPAND | wxALL, 2); + + panelSizer->Add(boxSizer, 1, wxEXPAND | wxALL, 2); + panel->SetSizerAndFit(panelSizer); + + return panel; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, wxStaticBox* box) { auto* row = new wxBoxSizer(wxHORIZONTAL); row->Add(new wxStaticText(box, wxID_ANY, fmt::format("{} {}", _("Skylander").ToStdString(), - (row_number + 1))), + (rowNumber + 1))), 1, wxEXPAND | wxALL, 2); - m_skylanderSlots[row_number] = + m_skylanderSlots[rowNumber] = new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, wxTE_READONLY); - m_skylanderSlots[row_number]->SetMinSize(wxSize(150, -1)); - m_skylanderSlots[row_number]->Disable(); - row->Add(m_skylanderSlots[row_number], 1, wxEXPAND | wxALL, 2); + m_skylanderSlots[rowNumber]->SetMinSize(wxSize(150, -1)); + m_skylanderSlots[rowNumber]->Disable(); + row->Add(m_skylanderSlots[rowNumber], 1, wxEXPAND | wxALL, 2); auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); - loadButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { - LoadSkylander(row_number); + loadButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + LoadSkylander(rowNumber); }); auto* createButton = new wxButton(box, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { - CreateSkylander(row_number); + createButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + CreateSkylander(rowNumber); }); auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); - clearButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { - ClearSkylander(row_number); + clearButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + ClearSkylander(rowNumber); + }); + row->Add(loadButton, 1, wxEXPAND | wxALL, 2); + row->Add(createButton, 1, wxEXPAND | wxALL, 2); + row->Add(clearButton, 1, wxEXPAND | wxALL, 2); + + return row; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddInfinityRow(wxString name, uint8 rowNumber, wxStaticBox* box) +{ + auto* row = new wxBoxSizer(wxHORIZONTAL); + + row->Add(new wxStaticText(box, wxID_ANY, name), 1, wxEXPAND | wxALL, 2); + m_infinitySlots[rowNumber] = + new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, + wxTE_READONLY); + m_infinitySlots[rowNumber]->SetMinSize(wxSize(150, -1)); + m_infinitySlots[rowNumber]->Disable(); + row->Add(m_infinitySlots[rowNumber], 1, wxALL | wxEXPAND, 5); + auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); + loadButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + LoadFigure(rowNumber); + }); + auto* createButton = new wxButton(box, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + CreateFigure(rowNumber); + }); + auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); + clearButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + ClearFigure(rowNumber); }); row->Add(loadButton, 1, wxEXPAND | wxALL, 2); row->Add(createButton, 1, wxEXPAND | wxALL, 2); @@ -138,7 +205,7 @@ void EmulatedUSBDeviceFrame::LoadSkylanderPath(uint8 slot, wxString path) return; } - std::array fileData; + std::array fileData; if (skyFile->readData(fileData.data(), fileData.size()) != fileData.size()) { wxMessageDialog open_error(this, "Failed to read file! File was too small"); @@ -218,15 +285,15 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) long longSkyId; if (!editId->GetValue().ToLong(&longSkyId) || longSkyId > 0xFFFF) { - wxMessageDialog id_error(this, "Error Converting ID!", "ID Entered is Invalid"); - id_error.ShowModal(); + wxMessageDialog idError(this, "Error Converting ID!", "ID Entered is Invalid"); + idError.ShowModal(); return; } long longSkyVar; if (!editVar->GetValue().ToLong(&longSkyVar) || longSkyVar > 0xFFFF) { - wxMessageDialog id_error(this, "Error Converting Variant!", "Variant Entered is Invalid"); - id_error.ShowModal(); + wxMessageDialog idError(this, "Error Converting Variant!", "Variant Entered is Invalid"); + idError.ShowModal(); return; } uint16 skyId = longSkyId & 0xFFFF; @@ -284,6 +351,157 @@ wxString CreateSkylanderDialog::GetFilePath() const return m_filePath; } +CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 slot) + : wxDialog(parent, wxID_ANY, _("Infinity Figure Creator"), wxDefaultPosition, wxSize(500, 150)) +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* comboRow = new wxBoxSizer(wxHORIZONTAL); + + auto* comboBox = new wxComboBox(this, wxID_ANY); + comboBox->Append("---Select---", reinterpret_cast(0xFFFFFF)); + wxArrayString filterlist; + for (const auto& it : nsyshid::g_infinitybase.GetFigureList()) + { + const uint32 figure = it.first; + if ((slot == 0 && + ((figure > 0x1E8480 && figure < 0x2DC6BF) || (figure > 0x3D0900 && figure < 0x4C4B3F))) || + ((slot == 1 || slot == 2) && (figure > 0x3D0900 && figure < 0x4C4B3F)) || + ((slot == 3 || slot == 6) && figure < 0x1E847F) || + ((slot == 4 || slot == 5 || slot == 7 || slot == 8) && + (figure > 0x2DC6C0 && figure < 0x3D08FF))) + { + comboBox->Append(it.second.second, reinterpret_cast(figure)); + filterlist.Add(it.second.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)) + { + wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); + idError.ShowModal(); + this->EndModal(0);; + } + uint32 figNum = longFigNum & 0xFFFFFFFF; + auto figure = nsyshid::g_infinitybase.FindFigure(figNum); + wxString predefName = figure.second + ".bin"; + wxFileDialog + saveFileDialog(this, _("Create Infinity 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_infinitybase.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum, figure.first); + + 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 != 0xFFFFFF) + { + const uint32 figNum = fig_info & 0xFFFFFFFF; + + 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 CreateInfinityFigureDialog::GetFilePath() const +{ + return m_filePath; +} + +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); +} + void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() { for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h index 8988cb8a..ae29a036 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h @@ -5,6 +5,7 @@ #include #include +#include "Cafe/OS/libs/nsyshid/Infinity.h" #include "Cafe/OS/libs/nsyshid/Skylander.h" class wxBoxSizer; @@ -23,15 +24,23 @@ class EmulatedUSBDeviceFrame : public wxFrame { private: wxCheckBox* m_emulatePortal; + wxCheckBox* m_emulateBase; std::array m_skylanderSlots; + std::array m_infinitySlots; std::array>, nsyshid::MAX_SKYLANDERS> m_skySlots; wxPanel* AddSkylanderPage(wxNotebook* notebook); + wxPanel* AddInfinityPage(wxNotebook* notebook); wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); + wxBoxSizer* AddInfinityRow(wxString name, uint8 row_number, wxStaticBox* box); void LoadSkylander(uint8 slot); void LoadSkylanderPath(uint8 slot, wxString path); void CreateSkylander(uint8 slot); void ClearSkylander(uint8 slot); + void LoadFigure(uint8 slot); + void LoadFigurePath(uint8 slot, wxString path); + void CreateFigure(uint8 slot); + void ClearFigure(uint8 slot); void UpdateSkylanderEdits(); }; class CreateSkylanderDialog : public wxDialog { @@ -39,6 +48,15 @@ class CreateSkylanderDialog : public wxDialog { explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot); wxString GetFilePath() const; + protected: + wxString m_filePath; +}; + +class CreateInfinityFigureDialog : public wxDialog { + public: + explicit CreateInfinityFigureDialog(wxWindow* parent, uint8 slot); + wxString GetFilePath() const; + protected: wxString m_filePath; }; \ No newline at end of file From e65abf48983f92a8de5259362f9842dbf3c28fb4 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 23 Jul 2024 21:18:55 +0100 Subject: [PATCH 34/73] Suppress unnecessary GTK messages (#1267) --- src/gui/CemuApp.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index baa83888..f91c1e3a 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -235,6 +235,9 @@ void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) bool CemuApp::OnInit() { +#if __WXGTK__ + GTKSuppressDiagnostics(G_LOG_LEVEL_MASK & ~G_LOG_FLAG_FATAL); +#endif std::set failedWriteAccess; DeterminePaths(failedWriteAccess); // make sure default cemu directories exist From 4b9c7c0d307495c679127381d6f00bab9f0c2933 Mon Sep 17 00:00:00 2001 From: Exverge Date: Wed, 24 Jul 2024 02:32:40 -0400 Subject: [PATCH 35/73] Update Fedora build instructions (#1269) --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 3ff2254f..1e92527e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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` +`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` ### Build Cemu From f1685eab665e1b262b47d6ea0c47d691fcc0f4a6 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:48:42 +0200 Subject: [PATCH 36/73] h264: Use asynchronous decoding when possible (#1257) --- src/Cafe/CMakeLists.txt | 2 + .../OS/libs/coreinit/coreinit_SysHeap.cpp | 12 +- src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h | 3 + src/Cafe/OS/libs/h264_avc/H264Dec.cpp | 755 +++--------------- .../OS/libs/h264_avc/H264DecBackendAVC.cpp | 502 ++++++++++++ src/Cafe/OS/libs/h264_avc/H264DecInternal.h | 139 ++++ .../OS/libs/h264_avc/parser/H264Parser.cpp | 17 +- src/Cafe/OS/libs/h264_avc/parser/H264Parser.h | 2 + 8 files changed, 787 insertions(+), 645 deletions(-) create mode 100644 src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp create mode 100644 src/Cafe/OS/libs/h264_avc/H264DecInternal.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 0fb7a44b..91d257b2 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -374,7 +374,9 @@ add_library(CemuCafe OS/libs/gx2/GX2_Texture.h OS/libs/gx2/GX2_TilingAperture.cpp OS/libs/h264_avc/H264Dec.cpp + OS/libs/h264_avc/H264DecBackendAVC.cpp OS/libs/h264_avc/h264dec.h + OS/libs/h264_avc/H264DecInternal.h OS/libs/h264_avc/parser OS/libs/h264_avc/parser/H264Parser.cpp OS/libs/h264_avc/parser/H264Parser.h diff --git a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp index e37949d7..2f819c50 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp @@ -14,13 +14,10 @@ namespace coreinit return coreinit::MEMAllocFromExpHeapEx(_sysHeapHandle, size, alignment); } - void export_OSAllocFromSystem(PPCInterpreter_t* hCPU) + void OSFreeToSystem(void* ptr) { - ppcDefineParamU32(size, 0); - ppcDefineParamS32(alignment, 1); - MEMPTR mem = OSAllocFromSystem(size, alignment); - cemuLog_logDebug(LogType::Force, "OSAllocFromSystem(0x{:x}, {}) -> 0x{:08x}", size, alignment, mem.GetMPTR()); - osLib_returnFromFunction(hCPU, mem.GetMPTR()); + _sysHeapFreeCounter++; + coreinit::MEMFreeToExpHeap(_sysHeapHandle, ptr); } void InitSysHeap() @@ -34,7 +31,8 @@ namespace coreinit void InitializeSysHeap() { - osLib_addFunction("coreinit", "OSAllocFromSystem", export_OSAllocFromSystem); + cafeExportRegister("h264", OSAllocFromSystem, LogType::CoreinitMem); + cafeExportRegister("h264", OSFreeToSystem, LogType::CoreinitMem); } } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h index 428224af..ad115754 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h @@ -4,5 +4,8 @@ namespace coreinit { void InitSysHeap(); + void* OSAllocFromSystem(uint32 size, uint32 alignment); + void OSFreeToSystem(void* ptr); + void InitializeSysHeap(); } \ No newline at end of file diff --git a/src/Cafe/OS/libs/h264_avc/H264Dec.cpp b/src/Cafe/OS/libs/h264_avc/H264Dec.cpp index 024965fd..82db039b 100644 --- a/src/Cafe/OS/libs/h264_avc/H264Dec.cpp +++ b/src/Cafe/OS/libs/h264_avc/H264Dec.cpp @@ -1,17 +1,12 @@ #include "Cafe/OS/common/OSCommon.h" #include "Cafe/HW/Espresso/PPCCallback.h" #include "Cafe/OS/libs/h264_avc/parser/H264Parser.h" +#include "Cafe/OS/libs/h264_avc/H264DecInternal.h" #include "util/highresolutiontimer/HighResolutionTimer.h" #include "Cafe/CafeSystem.h" #include "h264dec.h" -extern "C" -{ -#include "../dependencies/ih264d/common/ih264_typedefs.h" -#include "../dependencies/ih264d/decoder/ih264d.h" -}; - enum class H264DEC_STATUS : uint32 { SUCCESS = 0x0, @@ -33,10 +28,35 @@ namespace H264 return false; } + struct H264Context + { + struct + { + MEMPTR ptr{ nullptr }; + uint32be length{ 0 }; + float64be timestamp; + }BitStream; + struct + { + MEMPTR outputFunc{ nullptr }; + uint8be outputPerFrame{ 0 }; // whats the default? + MEMPTR userMemoryParam{ nullptr }; + }Param; + // misc + uint32be sessionHandle; + + // decoder state + struct + { + uint32 numFramesInFlight{0}; + }decoderState; + }; + uint32 H264DECMemoryRequirement(uint32 codecProfile, uint32 codecLevel, uint32 width, uint32 height, uint32be* sizeRequirementOut) { if (H264_IsBotW()) { + static_assert(sizeof(H264Context) < 256); *sizeRequirementOut = 256; return 0; } @@ -169,590 +189,47 @@ namespace H264 return H264DEC_STATUS::BAD_STREAM; } - struct H264Context - { - struct - { - MEMPTR ptr{ nullptr }; - uint32be length{ 0 }; - float64be timestamp; - }BitStream; - struct - { - MEMPTR outputFunc{ nullptr }; - uint8be outputPerFrame{ 0 }; // whats the default? - MEMPTR userMemoryParam{ nullptr }; - }Param; - // misc - uint32be sessionHandle; - }; - - class H264AVCDecoder - { - static void* ivd_aligned_malloc(void* ctxt, WORD32 alignment, WORD32 size) - { -#ifdef _WIN32 - return _aligned_malloc(size, alignment); -#else - // alignment is atleast sizeof(void*) - alignment = std::max(alignment, sizeof(void*)); - - //smallest multiple of 2 at least as large as alignment - alignment--; - alignment |= alignment << 1; - alignment |= alignment >> 1; - alignment |= alignment >> 2; - alignment |= alignment >> 4; - alignment |= alignment >> 8; - alignment |= alignment >> 16; - alignment ^= (alignment >> 1); - - void* temp; - posix_memalign(&temp, (size_t)alignment, (size_t)size); - return temp; -#endif - } - - static void ivd_aligned_free(void* ctxt, void* buf) - { -#ifdef _WIN32 - _aligned_free(buf); -#else - free(buf); -#endif - return; - } - - public: - struct DecodeResult - { - bool frameReady{ false }; - double timestamp; - void* imageOutput; - ivd_video_decode_op_t decodeOutput; - }; - - void Init(bool isBufferedMode) - { - ih264d_create_ip_t s_create_ip{ 0 }; - ih264d_create_op_t s_create_op{ 0 }; - - s_create_ip.s_ivd_create_ip_t.u4_size = sizeof(ih264d_create_ip_t); - s_create_ip.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE; - s_create_ip.s_ivd_create_ip_t.u4_share_disp_buf = 1; // shared display buffer mode -> We give the decoder a list of buffers that it will use (?) - - s_create_op.s_ivd_create_op_t.u4_size = sizeof(ih264d_create_op_t); - s_create_ip.s_ivd_create_ip_t.e_output_format = IV_YUV_420SP_UV; - s_create_ip.s_ivd_create_ip_t.pf_aligned_alloc = ivd_aligned_malloc; - s_create_ip.s_ivd_create_ip_t.pf_aligned_free = ivd_aligned_free; - s_create_ip.s_ivd_create_ip_t.pv_mem_ctxt = NULL; - - WORD32 status = ih264d_api_function(m_codecCtx, &s_create_ip, &s_create_op); - cemu_assert(!status); - - m_codecCtx = (iv_obj_t*)s_create_op.s_ivd_create_op_t.pv_handle; - m_codecCtx->pv_fxns = (void*)&ih264d_api_function; - m_codecCtx->u4_size = sizeof(iv_obj_t); - - SetDecoderCoreCount(1); - - m_isBufferedMode = isBufferedMode; - - UpdateParameters(false); - - m_bufferedResults.clear(); - m_numDecodedFrames = 0; - m_hasBufferSizeInfo = false; - m_timestampIndex = 0; - } - - void Destroy() - { - if (!m_codecCtx) - return; - ih264d_delete_ip_t s_delete_ip{ 0 }; - ih264d_delete_op_t s_delete_op{ 0 }; - s_delete_ip.s_ivd_delete_ip_t.u4_size = sizeof(ih264d_delete_ip_t); - s_delete_ip.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE; - s_delete_op.s_ivd_delete_op_t.u4_size = sizeof(ih264d_delete_op_t); - WORD32 status = ih264d_api_function(m_codecCtx, &s_delete_ip, &s_delete_op); - cemu_assert_debug(!status); - m_codecCtx = nullptr; - } - - void SetDecoderCoreCount(uint32 coreCount) - { - ih264d_ctl_set_num_cores_ip_t s_set_cores_ip; - ih264d_ctl_set_num_cores_op_t s_set_cores_op; - s_set_cores_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_set_cores_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_SET_NUM_CORES; - s_set_cores_ip.u4_num_cores = coreCount; // valid numbers are 1-4 - s_set_cores_ip.u4_size = sizeof(ih264d_ctl_set_num_cores_ip_t); - s_set_cores_op.u4_size = sizeof(ih264d_ctl_set_num_cores_op_t); - IV_API_CALL_STATUS_T status = ih264d_api_function(m_codecCtx, (void *)&s_set_cores_ip, (void *)&s_set_cores_op); - cemu_assert(status == IV_SUCCESS); - } - - static bool GetImageInfo(uint8* stream, uint32 length, uint32& imageWidth, uint32& imageHeight) - { - // create temporary decoder - ih264d_create_ip_t s_create_ip{ 0 }; - ih264d_create_op_t s_create_op{ 0 }; - s_create_ip.s_ivd_create_ip_t.u4_size = sizeof(ih264d_create_ip_t); - s_create_ip.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE; - s_create_ip.s_ivd_create_ip_t.u4_share_disp_buf = 0; - s_create_op.s_ivd_create_op_t.u4_size = sizeof(ih264d_create_op_t); - s_create_ip.s_ivd_create_ip_t.e_output_format = IV_YUV_420SP_UV; - s_create_ip.s_ivd_create_ip_t.pf_aligned_alloc = ivd_aligned_malloc; - s_create_ip.s_ivd_create_ip_t.pf_aligned_free = ivd_aligned_free; - s_create_ip.s_ivd_create_ip_t.pv_mem_ctxt = NULL; - iv_obj_t* ctx = nullptr; - WORD32 status = ih264d_api_function(ctx, &s_create_ip, &s_create_op); - cemu_assert_debug(!status); - if (status != IV_SUCCESS) - return false; - ctx = (iv_obj_t*)s_create_op.s_ivd_create_op_t.pv_handle; - ctx->pv_fxns = (void*)&ih264d_api_function; - ctx->u4_size = sizeof(iv_obj_t); - // set header-only mode - ih264d_ctl_set_config_ip_t s_h264d_ctl_ip{ 0 }; - ih264d_ctl_set_config_op_t s_h264d_ctl_op{ 0 }; - ivd_ctl_set_config_ip_t* ps_ctl_ip = &s_h264d_ctl_ip.s_ivd_ctl_set_config_ip_t; - ivd_ctl_set_config_op_t* ps_ctl_op = &s_h264d_ctl_op.s_ivd_ctl_set_config_op_t; - ps_ctl_ip->u4_disp_wd = 0; - ps_ctl_ip->e_frm_skip_mode = IVD_SKIP_NONE; - ps_ctl_ip->e_frm_out_mode = IVD_DISPLAY_FRAME_OUT; - ps_ctl_ip->e_vid_dec_mode = IVD_DECODE_HEADER; - ps_ctl_ip->e_cmd = IVD_CMD_VIDEO_CTL; - ps_ctl_ip->e_sub_cmd = IVD_CMD_CTL_SETPARAMS; - ps_ctl_ip->u4_size = sizeof(ih264d_ctl_set_config_ip_t); - ps_ctl_op->u4_size = sizeof(ih264d_ctl_set_config_op_t); - status = ih264d_api_function(ctx, &s_h264d_ctl_ip, &s_h264d_ctl_op); - cemu_assert(!status); - // decode stream - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - s_dec_ip.pv_stream_buffer = stream; - s_dec_ip.u4_num_Bytes = length; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - - s_dec_op.u4_raw_wd = 0; - s_dec_op.u4_raw_ht = 0; - - status = ih264d_api_function(ctx, &s_dec_ip, &s_dec_op); - //cemu_assert(status == 0); -> This errors when not both the headers are present, but it will still set the parameters we need - bool isValid = false; - if (true)//status == 0) - { - imageWidth = s_dec_op.u4_raw_wd; - imageHeight = s_dec_op.u4_raw_ht; - cemu_assert_debug(imageWidth != 0 && imageHeight != 0); - isValid = true; - } - // destroy decoder - ih264d_delete_ip_t s_delete_ip{ 0 }; - ih264d_delete_op_t s_delete_op{ 0 }; - s_delete_ip.s_ivd_delete_ip_t.u4_size = sizeof(ih264d_delete_ip_t); - s_delete_ip.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE; - s_delete_op.s_ivd_delete_op_t.u4_size = sizeof(ih264d_delete_op_t); - status = ih264d_api_function(ctx, &s_delete_ip, &s_delete_op); - cemu_assert_debug(!status); - return isValid; - } - - void Decode(void* data, uint32 length, double timestamp, void* imageOutput, DecodeResult& decodeResult) - { - if (!m_hasBufferSizeInfo) - { - uint32 numByteConsumed = 0; - if (!DetermineBufferSizes(data, length, numByteConsumed)) - { - cemuLog_log(LogType::Force, "H264: Unable to determine picture size. Ignoring decode input"); - decodeResult.frameReady = false; - return; - } - length -= numByteConsumed; - data = (uint8*)data + numByteConsumed; - m_hasBufferSizeInfo = true; - } - - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - - // remember timestamp and associated output buffer - m_timestamps[m_timestampIndex] = timestamp; - m_imageBuffers[m_timestampIndex] = imageOutput; - s_dec_ip.u4_ts = m_timestampIndex; - m_timestampIndex = (m_timestampIndex + 1) % 64; - - s_dec_ip.pv_stream_buffer = (uint8*)data; - s_dec_ip.u4_num_Bytes = length; - - s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; - s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - - BenchmarkTimer bt; - bt.Start(); - WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); - if (status != 0 && (s_dec_op.u4_error_code&0xFF) == IVD_RES_CHANGED) - { - // resolution change - ResetDecoder(); - m_hasBufferSizeInfo = false; - Decode(data, length, timestamp, imageOutput, decodeResult); - return; - } - else if (status != 0) - { - cemuLog_log(LogType::Force, "H264: Failed to decode frame (error 0x{:08x})", status); - decodeResult.frameReady = false; - return; - } - - bt.Stop(); - double decodeTime = bt.GetElapsedMilliseconds(); - - cemu_assert(s_dec_op.u4_frame_decoded_flag); - cemu_assert_debug(s_dec_op.u4_num_bytes_consumed == length); - - cemu_assert_debug(m_isBufferedMode || s_dec_op.u4_output_present); // if buffered mode is disabled, then every input should output a frame (except for partial slices?) - - if (s_dec_op.u4_output_present) - { - cemu_assert(s_dec_op.e_output_format == IV_YUV_420SP_UV); - if (H264_IsBotW()) - { - if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) - s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; - } - DecodeResult tmpResult; - tmpResult.frameReady = s_dec_op.u4_output_present != 0; - tmpResult.timestamp = m_timestamps[s_dec_op.u4_ts]; - tmpResult.imageOutput = m_imageBuffers[s_dec_op.u4_ts]; - tmpResult.decodeOutput = s_dec_op; - AddBufferedResult(tmpResult); - // transfer image to PPC output buffer and also correct stride - bt.Start(); - CopyImageToResultBuffer((uint8*)s_dec_op.s_disp_frm_buf.pv_y_buf, (uint8*)s_dec_op.s_disp_frm_buf.pv_u_buf, (uint8*)m_imageBuffers[s_dec_op.u4_ts], s_dec_op); - bt.Stop(); - double copyTime = bt.GetElapsedMilliseconds(); - // release buffer - sint32 bufferId = -1; - for (size_t i = 0; i < m_displayBuf.size(); i++) - { - if (s_dec_op.s_disp_frm_buf.pv_y_buf >= m_displayBuf[i].data() && s_dec_op.s_disp_frm_buf.pv_y_buf < (m_displayBuf[i].data() + m_displayBuf[i].size())) - { - bufferId = (sint32)i; - break; - } - } - cemu_assert_debug(bufferId == s_dec_op.u4_disp_buf_id); - cemu_assert(bufferId >= 0); - ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; - ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; - s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; - s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); - s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); - s_video_rel_disp_ip.u4_disp_buf_id = bufferId; - status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); - cemu_assert(!status); - - cemuLog_log(LogType::H264, "H264Bench | DecodeTime {}ms CopyTime {}ms", decodeTime, copyTime); - } - else - { - cemuLog_log(LogType::H264, "H264Bench | DecodeTime{}ms", decodeTime); - } - - if (s_dec_op.u4_frame_decoded_flag) - m_numDecodedFrames++; - - if (m_isBufferedMode) - { - // in buffered mode, always buffer 5 frames regardless of actual reordering and decoder latency - if (m_numDecodedFrames > 5) - GetCurrentBufferedResult(decodeResult); - } - else if(m_numDecodedFrames > 0) - GetCurrentBufferedResult(decodeResult); - - // get VUI - //ih264d_ctl_get_vui_params_ip_t s_ctl_get_vui_params_ip; - //ih264d_ctl_get_vui_params_op_t s_ctl_get_vui_params_op; - - //s_ctl_get_vui_params_ip.e_cmd = IVD_CMD_VIDEO_CTL; - //s_ctl_get_vui_params_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_GET_VUI_PARAMS; - //s_ctl_get_vui_params_ip.u4_size = sizeof(ih264d_ctl_get_vui_params_ip_t); - //s_ctl_get_vui_params_op.u4_size = sizeof(ih264d_ctl_get_vui_params_op_t); - - //status = ih264d_api_function(mCodecCtx, &s_ctl_get_vui_params_ip, &s_ctl_get_vui_params_op); - //cemu_assert(status == 0); - } - - std::vector Flush() - { - std::vector results; - // set flush mode - ivd_ctl_flush_ip_t s_video_flush_ip{ 0 }; - ivd_ctl_flush_op_t s_video_flush_op{ 0 }; - s_video_flush_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_video_flush_ip.e_sub_cmd = IVD_CMD_CTL_FLUSH; - s_video_flush_ip.u4_size = sizeof(ivd_ctl_flush_ip_t); - s_video_flush_op.u4_size = sizeof(ivd_ctl_flush_op_t); - WORD32 status = ih264d_api_function(m_codecCtx, &s_video_flush_ip, &s_video_flush_op); - if (status != 0) - cemuLog_log(LogType::Force, "H264Dec: Unexpected error during flush ({})", status); - // get all frames from the codec - while (true) - { - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - s_dec_ip.pv_stream_buffer = NULL; - s_dec_ip.u4_num_Bytes = 0; - s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; - s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); - if (status != 0) - break; - cemu_assert_debug(s_dec_op.u4_output_present != 0); // should never be zero? - if(s_dec_op.u4_output_present == 0) - continue; - if (H264_IsBotW()) - { - if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) - s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; - } - DecodeResult tmpResult; - tmpResult.frameReady = s_dec_op.u4_output_present != 0; - tmpResult.timestamp = m_timestamps[s_dec_op.u4_ts]; - tmpResult.imageOutput = m_imageBuffers[s_dec_op.u4_ts]; - tmpResult.decodeOutput = s_dec_op; - AddBufferedResult(tmpResult); - CopyImageToResultBuffer((uint8*)s_dec_op.s_disp_frm_buf.pv_y_buf, (uint8*)s_dec_op.s_disp_frm_buf.pv_u_buf, (uint8*)m_imageBuffers[s_dec_op.u4_ts], s_dec_op); - } - results = std::move(m_bufferedResults); - return results; - } - - void CopyImageToResultBuffer(uint8* yIn, uint8* uvIn, uint8* bufOut, ivd_video_decode_op_t& decodeInfo) - { - uint32 imageWidth = decodeInfo.s_disp_frm_buf.u4_y_wd; - uint32 imageHeight = decodeInfo.s_disp_frm_buf.u4_y_ht; - - size_t inputStride = decodeInfo.s_disp_frm_buf.u4_y_strd; - size_t outputStride = (imageWidth + 0xFF) & ~0xFF; - - // copy Y - uint8* yOut = bufOut; - for (uint32 row = 0; row < imageHeight; row++) - { - memcpy(yOut, yIn, imageWidth); - yIn += inputStride; - yOut += outputStride; - } - - // copy UV - uint8* uvOut = bufOut + outputStride * imageHeight; - for (uint32 row = 0; row < imageHeight/2; row++) - { - memcpy(uvOut, uvIn, imageWidth); - uvIn += inputStride; - uvOut += outputStride; - } - } - - private: - - bool DetermineBufferSizes(void* data, uint32 length, uint32& numByteConsumed) - { - numByteConsumed = 0; - UpdateParameters(true); - - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - s_dec_ip.pv_stream_buffer = (uint8*)data; - s_dec_ip.u4_num_Bytes = length; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); - if (status != 0) - { - cemuLog_log(LogType::Force, "H264: Unable to determine buffer sizes for stream"); - return false; - } - numByteConsumed = s_dec_op.u4_num_bytes_consumed; - cemu_assert(status == 0); - if (s_dec_op.u4_pic_wd == 0 || s_dec_op.u4_pic_ht == 0) - return false; - UpdateParameters(false); - ReinitBuffers(); - return true; - } - - void ReinitBuffers() - { - ivd_ctl_getbufinfo_ip_t s_ctl_ip{ 0 }; - ivd_ctl_getbufinfo_op_t s_ctl_op{ 0 }; - WORD32 outlen = 0; - - s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_GETBUFINFO; - s_ctl_ip.u4_size = sizeof(ivd_ctl_getbufinfo_ip_t); - s_ctl_op.u4_size = sizeof(ivd_ctl_getbufinfo_op_t); - - WORD32 status = ih264d_api_function(m_codecCtx, &s_ctl_ip, &s_ctl_op); - cemu_assert(!status); - - // allocate - for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) - { - m_displayBuf.emplace_back().resize(s_ctl_op.u4_min_out_buf_size[0] + s_ctl_op.u4_min_out_buf_size[1]); - } - // set - ivd_set_display_frame_ip_t s_set_display_frame_ip{ 0 }; // make sure to zero-initialize this. The codec seems to check the first 3 pointers/sizes per frame, regardless of the value of u4_num_bufs - ivd_set_display_frame_op_t s_set_display_frame_op{ 0 }; - - s_set_display_frame_ip.e_cmd = IVD_CMD_SET_DISPLAY_FRAME; - s_set_display_frame_ip.u4_size = sizeof(ivd_set_display_frame_ip_t); - s_set_display_frame_op.u4_size = sizeof(ivd_set_display_frame_op_t); - - cemu_assert_debug(s_ctl_op.u4_min_num_out_bufs == 2); - cemu_assert_debug(s_ctl_op.u4_min_out_buf_size[0] != 0 && s_ctl_op.u4_min_out_buf_size[1] != 0); - - s_set_display_frame_ip.num_disp_bufs = s_ctl_op.u4_num_disp_bufs; - - for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) - { - s_set_display_frame_ip.s_disp_buffer[i].u4_num_bufs = 2; - s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[0] = s_ctl_op.u4_min_out_buf_size[0]; - s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[1] = s_ctl_op.u4_min_out_buf_size[1]; - s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[0] = m_displayBuf[i].data() + 0; - s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[1] = m_displayBuf[i].data() + s_ctl_op.u4_min_out_buf_size[0]; - } - - status = ih264d_api_function(m_codecCtx, &s_set_display_frame_ip, &s_set_display_frame_op); - cemu_assert(!status); - - - // mark all as released (available) - for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) - { - ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; - ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; - - s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; - s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); - s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); - s_video_rel_disp_ip.u4_disp_buf_id = i; - - status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); - cemu_assert(!status); - } - } - - void ResetDecoder() - { - ivd_ctl_reset_ip_t s_ctl_ip; - ivd_ctl_reset_op_t s_ctl_op; - - s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_RESET; - s_ctl_ip.u4_size = sizeof(ivd_ctl_reset_ip_t); - s_ctl_op.u4_size = sizeof(ivd_ctl_reset_op_t); - - WORD32 status = ih264d_api_function(m_codecCtx, (void*)&s_ctl_ip, (void*)&s_ctl_op); - cemu_assert_debug(status == 0); - } - - void UpdateParameters(bool headerDecodeOnly) - { - ih264d_ctl_set_config_ip_t s_h264d_ctl_ip{ 0 }; - ih264d_ctl_set_config_op_t s_h264d_ctl_op{ 0 }; - ivd_ctl_set_config_ip_t* ps_ctl_ip = &s_h264d_ctl_ip.s_ivd_ctl_set_config_ip_t; - ivd_ctl_set_config_op_t* ps_ctl_op = &s_h264d_ctl_op.s_ivd_ctl_set_config_op_t; - - ps_ctl_ip->u4_disp_wd = 0; - ps_ctl_ip->e_frm_skip_mode = IVD_SKIP_NONE; - ps_ctl_ip->e_frm_out_mode = m_isBufferedMode ? IVD_DISPLAY_FRAME_OUT : IVD_DECODE_FRAME_OUT; - ps_ctl_ip->e_vid_dec_mode = headerDecodeOnly ? IVD_DECODE_HEADER : IVD_DECODE_FRAME; - ps_ctl_ip->e_cmd = IVD_CMD_VIDEO_CTL; - ps_ctl_ip->e_sub_cmd = IVD_CMD_CTL_SETPARAMS; - ps_ctl_ip->u4_size = sizeof(ih264d_ctl_set_config_ip_t); - ps_ctl_op->u4_size = sizeof(ih264d_ctl_set_config_op_t); - - WORD32 status = ih264d_api_function(m_codecCtx, &s_h264d_ctl_ip, &s_h264d_ctl_op); - cemu_assert(status == 0); - } - - /* In non-flush mode we have a delay of (at least?) 5 frames */ - void AddBufferedResult(DecodeResult& decodeResult) - { - if (decodeResult.frameReady) - m_bufferedResults.emplace_back(decodeResult); - } - - void GetCurrentBufferedResult(DecodeResult& decodeResult) - { - cemu_assert(!m_bufferedResults.empty()); - if (m_bufferedResults.empty()) - { - decodeResult.frameReady = false; - return; - } - decodeResult = m_bufferedResults.front(); - m_bufferedResults.erase(m_bufferedResults.begin()); - } - private: - iv_obj_t* m_codecCtx{nullptr}; - bool m_hasBufferSizeInfo{ false }; - bool m_isBufferedMode{ false }; - double m_timestamps[64]; - void* m_imageBuffers[64]; - uint32 m_timestampIndex{0}; - std::vector m_bufferedResults; - uint32 m_numDecodedFrames{0}; - std::vector> m_displayBuf; - }; - H264DEC_STATUS H264DECGetImageSize(uint8* stream, uint32 length, uint32 offset, uint32be* outputWidth, uint32be* outputHeight) { - cemu_assert(offset <= length); - - uint32 imageWidth, imageHeight; - - if (H264AVCDecoder::GetImageInfo(stream, length, imageWidth, imageHeight)) + if(!stream || length < 4 || !outputWidth || !outputHeight) + return H264DEC_STATUS::INVALID_PARAM; + if( (offset+4) > length ) + return H264DEC_STATUS::INVALID_PARAM; + uint8* cur = stream + offset; + uint8* end = stream + length; + cur += 2; // we access cur[-2] and cur[-1] so we need to start at offset 2 + while(cur < end-2) { - if (H264_IsBotW()) + // check for start code + if(*cur != 1) { - if (imageWidth == 1920 && imageHeight == 1088) - imageHeight = 1080; + cur++; + continue; } - *outputWidth = imageWidth; - *outputHeight = imageHeight; + // check if this is a valid NAL header + if(cur[-2] != 0 || cur[-1] != 0 || cur[0] != 1) + { + cur++; + continue; + } + uint8 nalHeader = cur[1]; + if((nalHeader & 0x1F) != 7) + { + cur++; + continue; + } + h264State_seq_parameter_set_t psp; + bool r = h264Parser_ParseSPS(cur+2, end-cur-2, psp); + if(!r) + { + cemu_assert_suspicious(); // should not happen + return H264DEC_STATUS::BAD_STREAM; + } + *outputWidth = (psp.pic_width_in_mbs_minus1 + 1) * 16; + *outputHeight = (psp.pic_height_in_map_units_minus1 + 1) * 16; // affected by frame_mbs_only_flag? + return H264DEC_STATUS::SUCCESS; } - else - { - *outputWidth = 0; - *outputHeight = 0; - return H264DEC_STATUS::BAD_STREAM; - } - - return H264DEC_STATUS::SUCCESS; + return H264DEC_STATUS::BAD_STREAM; } uint32 H264DECInitParam(uint32 workMemorySize, void* workMemory) @@ -762,26 +239,28 @@ namespace H264 return 0; } - std::unordered_map sDecoderSessions; + std::unordered_map sDecoderSessions; std::mutex sDecoderSessionsMutex; std::atomic_uint32_t sCurrentSessionHandle{ 1 }; - static H264AVCDecoder* _CreateDecoderSession(uint32& handleOut) + H264DecoderBackend* CreateAVCDecoder(); + + static H264DecoderBackend* _CreateDecoderSession(uint32& handleOut) { std::unique_lock _lock(sDecoderSessionsMutex); handleOut = sCurrentSessionHandle.fetch_add(1); - H264AVCDecoder* session = new H264AVCDecoder(); + H264DecoderBackend* session = CreateAVCDecoder(); sDecoderSessions.try_emplace(handleOut, session); return session; } - static H264AVCDecoder* _AcquireDecoderSession(uint32 handle) + static H264DecoderBackend* _AcquireDecoderSession(uint32 handle) { std::unique_lock _lock(sDecoderSessionsMutex); auto it = sDecoderSessions.find(handle); if (it == sDecoderSessions.end()) return nullptr; - H264AVCDecoder* session = it->second; + H264DecoderBackend* session = it->second; if (sDecoderSessions.size() >= 5) { cemuLog_log(LogType::Force, "H264: Warning - more than 5 active sessions"); @@ -790,7 +269,7 @@ namespace H264 return session; } - static void _ReleaseDecoderSession(H264AVCDecoder* session) + static void _ReleaseDecoderSession(H264DecoderBackend* session) { std::unique_lock _lock(sDecoderSessionsMutex); @@ -802,7 +281,7 @@ namespace H264 auto it = sDecoderSessions.find(handle); if (it == sDecoderSessions.end()) return; - H264AVCDecoder* session = it->second; + H264DecoderBackend* session = it->second; session->Destroy(); delete session; sDecoderSessions.erase(it); @@ -830,45 +309,44 @@ namespace H264 uint32 H264DECBegin(void* workMemory) { H264Context* ctx = (H264Context*)workMemory; - H264AVCDecoder* session = _AcquireDecoderSession(ctx->sessionHandle); + H264DecoderBackend* session = _AcquireDecoderSession(ctx->sessionHandle); if (!session) { cemuLog_log(LogType::Force, "H264DECBegin(): Invalid session"); return 0; } session->Init(ctx->Param.outputPerFrame == 0); + ctx->decoderState.numFramesInFlight = 0; _ReleaseDecoderSession(session); return 0; } - void H264DoFrameOutputCallback(H264Context* ctx, H264AVCDecoder::DecodeResult& decodeResult); - - void _async_H264DECEnd(coreinit::OSEvent* executeDoneEvent, H264AVCDecoder* session, H264Context* ctx, std::vector* decodeResultsOut) - { - *decodeResultsOut = session->Flush(); - coreinit::OSSignalEvent(executeDoneEvent); - } + void H264DoFrameOutputCallback(H264Context* ctx, H264DecoderBackend::DecodeResult& decodeResult); H264DEC_STATUS H264DECEnd(void* workMemory) { H264Context* ctx = (H264Context*)workMemory; - H264AVCDecoder* session = _AcquireDecoderSession(ctx->sessionHandle); + H264DecoderBackend* session = _AcquireDecoderSession(ctx->sessionHandle); if (!session) { cemuLog_log(LogType::Force, "H264DECEnd(): Invalid session"); return H264DEC_STATUS::SUCCESS; } - StackAllocator executeDoneEvent; - coreinit::OSInitEvent(&executeDoneEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_MANUAL); - std::vector results; - auto asyncTask = std::async(std::launch::async, _async_H264DECEnd, executeDoneEvent.GetPointer(), session, ctx, &results); - coreinit::OSWaitEvent(&executeDoneEvent); - _ReleaseDecoderSession(session); - if (!results.empty()) + coreinit::OSEvent* flushEvt = &session->GetFlushEvent(); + coreinit::OSResetEvent(flushEvt); + session->QueueFlush(); + coreinit::OSWaitEvent(flushEvt); + while(true) { - for (auto& itr : results) - H264DoFrameOutputCallback(ctx, itr); + H264DecoderBackend::DecodeResult decodeResult; + if( !session->GetFrameOutputIfReady(decodeResult) ) + break; + // todo - output all frames in a single callback? + H264DoFrameOutputCallback(ctx, decodeResult); + ctx->decoderState.numFramesInFlight--; } + cemu_assert_debug(ctx->decoderState.numFramesInFlight == 0); // no frames should be in flight anymore. Exact behavior is not well understood but we may have to output dummy frames if necessary + _ReleaseDecoderSession(session); return H264DEC_STATUS::SUCCESS; } @@ -930,7 +408,6 @@ namespace H264 return 0; } - struct H264DECFrameOutput { /* +0x00 */ uint32be result; @@ -967,7 +444,7 @@ namespace H264 static_assert(sizeof(H264OutputCBStruct) == 12); - void H264DoFrameOutputCallback(H264Context* ctx, H264AVCDecoder::DecodeResult& decodeResult) + void H264DoFrameOutputCallback(H264Context* ctx, H264DecoderBackend::DecodeResult& decodeResult) { sint32 outputFrameCount = 1; @@ -984,14 +461,14 @@ namespace H264 frameOutput->imagePtr = (uint8*)decodeResult.imageOutput; frameOutput->result = 100; frameOutput->timestamp = decodeResult.timestamp; - frameOutput->frameWidth = decodeResult.decodeOutput.u4_pic_wd; - frameOutput->frameHeight = decodeResult.decodeOutput.u4_pic_ht; - frameOutput->bytesPerRow = (decodeResult.decodeOutput.u4_pic_wd + 0xFF) & ~0xFF; - frameOutput->cropEnable = decodeResult.decodeOutput.u1_frame_cropping_flag; - frameOutput->cropTop = decodeResult.decodeOutput.u1_frame_cropping_rect_top_ofst; - frameOutput->cropBottom = decodeResult.decodeOutput.u1_frame_cropping_rect_bottom_ofst; - frameOutput->cropLeft = decodeResult.decodeOutput.u1_frame_cropping_rect_left_ofst; - frameOutput->cropRight = decodeResult.decodeOutput.u1_frame_cropping_rect_right_ofst; + frameOutput->frameWidth = decodeResult.frameWidth; + frameOutput->frameHeight = decodeResult.frameHeight; + frameOutput->bytesPerRow = decodeResult.bytesPerRow; + frameOutput->cropEnable = decodeResult.cropEnable; + frameOutput->cropTop = decodeResult.cropTop; + frameOutput->cropBottom = decodeResult.cropBottom; + frameOutput->cropLeft = decodeResult.cropLeft; + frameOutput->cropRight = decodeResult.cropRight; StackAllocator stack_fptrOutputData; stack_fptrOutputData->frameCount = outputFrameCount; @@ -1006,29 +483,41 @@ namespace H264 } } - void _async_H264DECExecute(coreinit::OSEvent* executeDoneEvent, H264AVCDecoder* session, H264Context* ctx, void* imageOutput, H264AVCDecoder::DecodeResult* decodeResult) - { - session->Decode(ctx->BitStream.ptr.GetPtr(), ctx->BitStream.length, ctx->BitStream.timestamp, imageOutput, *decodeResult); - coreinit::OSSignalEvent(executeDoneEvent); - } - uint32 H264DECExecute(void* workMemory, void* imageOutput) { + BenchmarkTimer bt; + bt.Start(); H264Context* ctx = (H264Context*)workMemory; - H264AVCDecoder* session = _AcquireDecoderSession(ctx->sessionHandle); + H264DecoderBackend* session = _AcquireDecoderSession(ctx->sessionHandle); if (!session) { cemuLog_log(LogType::Force, "H264DECExecute(): Invalid session"); return 0; } - StackAllocator executeDoneEvent; - coreinit::OSInitEvent(&executeDoneEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_MANUAL); - H264AVCDecoder::DecodeResult decodeResult; - auto asyncTask = std::async(std::launch::async, _async_H264DECExecute, &executeDoneEvent, session, ctx, imageOutput , &decodeResult); - coreinit::OSWaitEvent(&executeDoneEvent); + // feed data to backend + session->QueueForDecode((uint8*)ctx->BitStream.ptr.GetPtr(), ctx->BitStream.length, ctx->BitStream.timestamp, imageOutput); + ctx->decoderState.numFramesInFlight++; + // H264DECExecute is synchronous and will return a frame after either every call (non-buffered) or after 6 calls (buffered) + // normally frame decoding happens only during H264DECExecute, but in order to hide the latency of our CPU decoder we will decode asynchronously in buffered mode + uint32 numFramesToBuffer = (ctx->Param.outputPerFrame == 0) ? 5 : 0; + if(ctx->decoderState.numFramesInFlight > numFramesToBuffer) + { + ctx->decoderState.numFramesInFlight--; + while(true) + { + coreinit::OSEvent& evt = session->GetFrameOutputEvent(); + coreinit::OSWaitEvent(&evt); + H264DecoderBackend::DecodeResult decodeResult; + if( !session->GetFrameOutputIfReady(decodeResult) ) + continue; + H264DoFrameOutputCallback(ctx, decodeResult); + break; + } + } _ReleaseDecoderSession(session); - if(decodeResult.frameReady) - H264DoFrameOutputCallback(ctx, decodeResult); + bt.Stop(); + double callTime = bt.GetElapsedMilliseconds(); + cemuLog_log(LogType::H264, "H264Bench | H264DECExecute took {}ms", callTime); return 0x80 | 100; } diff --git a/src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp b/src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp new file mode 100644 index 00000000..228f65a5 --- /dev/null +++ b/src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp @@ -0,0 +1,502 @@ +#include "H264DecInternal.h" +#include "util/highresolutiontimer/HighResolutionTimer.h" + +extern "C" +{ +#include "../dependencies/ih264d/common/ih264_typedefs.h" +#include "../dependencies/ih264d/decoder/ih264d.h" +}; + +namespace H264 +{ + bool H264_IsBotW(); + + class H264AVCDecoder : public H264DecoderBackend + { + static void* ivd_aligned_malloc(void* ctxt, WORD32 alignment, WORD32 size) + { +#ifdef _WIN32 + return _aligned_malloc(size, alignment); +#else + // alignment is atleast sizeof(void*) + alignment = std::max(alignment, sizeof(void*)); + + //smallest multiple of 2 at least as large as alignment + alignment--; + alignment |= alignment << 1; + alignment |= alignment >> 1; + alignment |= alignment >> 2; + alignment |= alignment >> 4; + alignment |= alignment >> 8; + alignment |= alignment >> 16; + alignment ^= (alignment >> 1); + + void* temp; + posix_memalign(&temp, (size_t)alignment, (size_t)size); + return temp; +#endif + } + + static void ivd_aligned_free(void* ctxt, void* buf) + { +#ifdef _WIN32 + _aligned_free(buf); +#else + free(buf); +#endif + } + + public: + H264AVCDecoder() + { + m_decoderThread = std::thread(&H264AVCDecoder::DecoderThread, this); + } + + ~H264AVCDecoder() + { + m_threadShouldExit = true; + m_decodeSem.increment(); + if (m_decoderThread.joinable()) + m_decoderThread.join(); + } + + void Init(bool isBufferedMode) + { + ih264d_create_ip_t s_create_ip{ 0 }; + ih264d_create_op_t s_create_op{ 0 }; + + s_create_ip.s_ivd_create_ip_t.u4_size = sizeof(ih264d_create_ip_t); + s_create_ip.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE; + s_create_ip.s_ivd_create_ip_t.u4_share_disp_buf = 1; // shared display buffer mode -> We give the decoder a list of buffers that it will use (?) + + s_create_op.s_ivd_create_op_t.u4_size = sizeof(ih264d_create_op_t); + s_create_ip.s_ivd_create_ip_t.e_output_format = IV_YUV_420SP_UV; + s_create_ip.s_ivd_create_ip_t.pf_aligned_alloc = ivd_aligned_malloc; + s_create_ip.s_ivd_create_ip_t.pf_aligned_free = ivd_aligned_free; + s_create_ip.s_ivd_create_ip_t.pv_mem_ctxt = NULL; + + WORD32 status = ih264d_api_function(m_codecCtx, &s_create_ip, &s_create_op); + cemu_assert(!status); + + m_codecCtx = (iv_obj_t*)s_create_op.s_ivd_create_op_t.pv_handle; + m_codecCtx->pv_fxns = (void*)&ih264d_api_function; + m_codecCtx->u4_size = sizeof(iv_obj_t); + + SetDecoderCoreCount(1); + + m_isBufferedMode = isBufferedMode; + + UpdateParameters(false); + + m_numDecodedFrames = 0; + m_hasBufferSizeInfo = false; + } + + void Destroy() + { + if (!m_codecCtx) + return; + ih264d_delete_ip_t s_delete_ip{ 0 }; + ih264d_delete_op_t s_delete_op{ 0 }; + s_delete_ip.s_ivd_delete_ip_t.u4_size = sizeof(ih264d_delete_ip_t); + s_delete_ip.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE; + s_delete_op.s_ivd_delete_op_t.u4_size = sizeof(ih264d_delete_op_t); + WORD32 status = ih264d_api_function(m_codecCtx, &s_delete_ip, &s_delete_op); + cemu_assert_debug(!status); + m_codecCtx = nullptr; + } + + void PushDecodedFrame(ivd_video_decode_op_t& s_dec_op) + { + // copy image data outside of lock since its an expensive operation + CopyImageToResultBuffer((uint8*)s_dec_op.s_disp_frm_buf.pv_y_buf, (uint8*)s_dec_op.s_disp_frm_buf.pv_u_buf, (uint8*)m_decodedSliceArray[s_dec_op.u4_ts].result.imageOutput, s_dec_op); + + std::unique_lock _l(m_decodeQueueMtx); + cemu_assert(s_dec_op.u4_ts < m_decodedSliceArray.size()); + auto& result = m_decodedSliceArray[s_dec_op.u4_ts]; + cemu_assert_debug(result.isUsed); + cemu_assert_debug(s_dec_op.u4_output_present != 0); + + result.result.isDecoded = true; + result.result.hasFrame = s_dec_op.u4_output_present != 0; + result.result.frameWidth = s_dec_op.u4_pic_wd; + result.result.frameHeight = s_dec_op.u4_pic_ht; + result.result.bytesPerRow = (s_dec_op.u4_pic_wd + 0xFF) & ~0xFF; + result.result.cropEnable = s_dec_op.u1_frame_cropping_flag; + result.result.cropTop = s_dec_op.u1_frame_cropping_rect_top_ofst; + result.result.cropBottom = s_dec_op.u1_frame_cropping_rect_bottom_ofst; + result.result.cropLeft = s_dec_op.u1_frame_cropping_rect_left_ofst; + result.result.cropRight = s_dec_op.u1_frame_cropping_rect_right_ofst; + + m_displayQueue.push_back(s_dec_op.u4_ts); + + _l.unlock(); + coreinit::OSSignalEvent(m_displayQueueEvt); + } + + // called from async worker thread + void Decode(DecodedSlice& decodedSlice) + { + if (!m_hasBufferSizeInfo) + { + uint32 numByteConsumed = 0; + if (!DetermineBufferSizes(decodedSlice.dataToDecode.m_data, decodedSlice.dataToDecode.m_length, numByteConsumed)) + { + cemuLog_log(LogType::Force, "H264AVC: Unable to determine picture size. Ignoring decode input"); + std::unique_lock _l(m_decodeQueueMtx); + decodedSlice.result.isDecoded = true; + decodedSlice.result.hasFrame = false; + coreinit::OSSignalEvent(m_displayQueueEvt); + return; + } + decodedSlice.dataToDecode.m_length -= numByteConsumed; + decodedSlice.dataToDecode.m_data = (uint8*)decodedSlice.dataToDecode.m_data + numByteConsumed; + m_hasBufferSizeInfo = true; + } + + ivd_video_decode_ip_t s_dec_ip{ 0 }; + ivd_video_decode_op_t s_dec_op{ 0 }; + s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); + s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); + + s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; + + s_dec_ip.u4_ts = std::distance(m_decodedSliceArray.data(), &decodedSlice); + cemu_assert_debug(s_dec_ip.u4_ts < m_decodedSliceArray.size()); + + s_dec_ip.pv_stream_buffer = (uint8*)decodedSlice.dataToDecode.m_data; + s_dec_ip.u4_num_Bytes = decodedSlice.dataToDecode.m_length; + + s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; + s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; + s_dec_ip.s_out_buffer.u4_num_bufs = 0; + + BenchmarkTimer bt; + bt.Start(); + WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); + if (status != 0 && (s_dec_op.u4_error_code&0xFF) == IVD_RES_CHANGED) + { + // resolution change + ResetDecoder(); + m_hasBufferSizeInfo = false; + Decode(decodedSlice); + return; + } + else if (status != 0) + { + cemuLog_log(LogType::Force, "H264: Failed to decode frame (error 0x{:08x})", status); + decodedSlice.result.hasFrame = false; + cemu_assert_unimplemented(); + return; + } + + bt.Stop(); + double decodeTime = bt.GetElapsedMilliseconds(); + + cemu_assert(s_dec_op.u4_frame_decoded_flag); + cemu_assert_debug(s_dec_op.u4_num_bytes_consumed == decodedSlice.dataToDecode.m_length); + + cemu_assert_debug(m_isBufferedMode || s_dec_op.u4_output_present); // if buffered mode is disabled, then every input should output a frame (except for partial slices?) + + if (s_dec_op.u4_output_present) + { + cemu_assert(s_dec_op.e_output_format == IV_YUV_420SP_UV); + if (H264_IsBotW()) + { + if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) + s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; + } + bt.Start(); + PushDecodedFrame(s_dec_op); + bt.Stop(); + double copyTime = bt.GetElapsedMilliseconds(); + // release buffer + sint32 bufferId = -1; + for (size_t i = 0; i < m_displayBuf.size(); i++) + { + if (s_dec_op.s_disp_frm_buf.pv_y_buf >= m_displayBuf[i].data() && s_dec_op.s_disp_frm_buf.pv_y_buf < (m_displayBuf[i].data() + m_displayBuf[i].size())) + { + bufferId = (sint32)i; + break; + } + } + cemu_assert_debug(bufferId == s_dec_op.u4_disp_buf_id); + cemu_assert(bufferId >= 0); + ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; + ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; + s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; + s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); + s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); + s_video_rel_disp_ip.u4_disp_buf_id = bufferId; + status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); + cemu_assert(!status); + + cemuLog_log(LogType::H264, "H264Bench | DecodeTime {}ms CopyTime {}ms", decodeTime, copyTime); + } + else + { + cemuLog_log(LogType::H264, "H264Bench | DecodeTime {}ms (no frame output)", decodeTime); + } + + if (s_dec_op.u4_frame_decoded_flag) + m_numDecodedFrames++; + // get VUI + //ih264d_ctl_get_vui_params_ip_t s_ctl_get_vui_params_ip; + //ih264d_ctl_get_vui_params_op_t s_ctl_get_vui_params_op; + + //s_ctl_get_vui_params_ip.e_cmd = IVD_CMD_VIDEO_CTL; + //s_ctl_get_vui_params_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_GET_VUI_PARAMS; + //s_ctl_get_vui_params_ip.u4_size = sizeof(ih264d_ctl_get_vui_params_ip_t); + //s_ctl_get_vui_params_op.u4_size = sizeof(ih264d_ctl_get_vui_params_op_t); + + //status = ih264d_api_function(mCodecCtx, &s_ctl_get_vui_params_ip, &s_ctl_get_vui_params_op); + //cemu_assert(status == 0); + } + + void Flush() + { + // set flush mode + ivd_ctl_flush_ip_t s_video_flush_ip{ 0 }; + ivd_ctl_flush_op_t s_video_flush_op{ 0 }; + s_video_flush_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_video_flush_ip.e_sub_cmd = IVD_CMD_CTL_FLUSH; + s_video_flush_ip.u4_size = sizeof(ivd_ctl_flush_ip_t); + s_video_flush_op.u4_size = sizeof(ivd_ctl_flush_op_t); + WORD32 status = ih264d_api_function(m_codecCtx, &s_video_flush_ip, &s_video_flush_op); + if (status != 0) + cemuLog_log(LogType::Force, "H264Dec: Unexpected error during flush ({})", status); + // get all frames from the decoder + while (true) + { + ivd_video_decode_ip_t s_dec_ip{ 0 }; + ivd_video_decode_op_t s_dec_op{ 0 }; + s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); + s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); + s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; + s_dec_ip.pv_stream_buffer = NULL; + s_dec_ip.u4_num_Bytes = 0; + s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; + s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; + s_dec_ip.s_out_buffer.u4_num_bufs = 0; + status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); + if (status != 0) + break; + cemu_assert_debug(s_dec_op.u4_output_present != 0); // should never be false? + if(s_dec_op.u4_output_present == 0) + continue; + if (H264_IsBotW()) + { + if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) + s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; + } + PushDecodedFrame(s_dec_op); + } + } + + void CopyImageToResultBuffer(uint8* yIn, uint8* uvIn, uint8* bufOut, ivd_video_decode_op_t& decodeInfo) + { + uint32 imageWidth = decodeInfo.s_disp_frm_buf.u4_y_wd; + uint32 imageHeight = decodeInfo.s_disp_frm_buf.u4_y_ht; + + size_t inputStride = decodeInfo.s_disp_frm_buf.u4_y_strd; + size_t outputStride = (imageWidth + 0xFF) & ~0xFF; + + // copy Y + uint8* yOut = bufOut; + for (uint32 row = 0; row < imageHeight; row++) + { + memcpy(yOut, yIn, imageWidth); + yIn += inputStride; + yOut += outputStride; + } + + // copy UV + uint8* uvOut = bufOut + outputStride * imageHeight; + for (uint32 row = 0; row < imageHeight/2; row++) + { + memcpy(uvOut, uvIn, imageWidth); + uvIn += inputStride; + uvOut += outputStride; + } + } + private: + void SetDecoderCoreCount(uint32 coreCount) + { + ih264d_ctl_set_num_cores_ip_t s_set_cores_ip; + ih264d_ctl_set_num_cores_op_t s_set_cores_op; + s_set_cores_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_set_cores_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_SET_NUM_CORES; + s_set_cores_ip.u4_num_cores = coreCount; // valid numbers are 1-4 + s_set_cores_ip.u4_size = sizeof(ih264d_ctl_set_num_cores_ip_t); + s_set_cores_op.u4_size = sizeof(ih264d_ctl_set_num_cores_op_t); + IV_API_CALL_STATUS_T status = ih264d_api_function(m_codecCtx, (void *)&s_set_cores_ip, (void *)&s_set_cores_op); + cemu_assert(status == IV_SUCCESS); + } + + bool DetermineBufferSizes(void* data, uint32 length, uint32& numByteConsumed) + { + numByteConsumed = 0; + UpdateParameters(true); + + ivd_video_decode_ip_t s_dec_ip{ 0 }; + ivd_video_decode_op_t s_dec_op{ 0 }; + s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); + s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); + + s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; + s_dec_ip.pv_stream_buffer = (uint8*)data; + s_dec_ip.u4_num_Bytes = length; + s_dec_ip.s_out_buffer.u4_num_bufs = 0; + WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); + if (status != 0) + { + cemuLog_log(LogType::Force, "H264: Unable to determine buffer sizes for stream"); + return false; + } + numByteConsumed = s_dec_op.u4_num_bytes_consumed; + cemu_assert(status == 0); + if (s_dec_op.u4_pic_wd == 0 || s_dec_op.u4_pic_ht == 0) + return false; + UpdateParameters(false); + ReinitBuffers(); + return true; + } + + void ReinitBuffers() + { + ivd_ctl_getbufinfo_ip_t s_ctl_ip{ 0 }; + ivd_ctl_getbufinfo_op_t s_ctl_op{ 0 }; + WORD32 outlen = 0; + + s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_GETBUFINFO; + s_ctl_ip.u4_size = sizeof(ivd_ctl_getbufinfo_ip_t); + s_ctl_op.u4_size = sizeof(ivd_ctl_getbufinfo_op_t); + + WORD32 status = ih264d_api_function(m_codecCtx, &s_ctl_ip, &s_ctl_op); + cemu_assert(!status); + + // allocate + for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) + { + m_displayBuf.emplace_back().resize(s_ctl_op.u4_min_out_buf_size[0] + s_ctl_op.u4_min_out_buf_size[1]); + } + // set + ivd_set_display_frame_ip_t s_set_display_frame_ip{ 0 }; // make sure to zero-initialize this. The codec seems to check the first 3 pointers/sizes per frame, regardless of the value of u4_num_bufs + ivd_set_display_frame_op_t s_set_display_frame_op{ 0 }; + + s_set_display_frame_ip.e_cmd = IVD_CMD_SET_DISPLAY_FRAME; + s_set_display_frame_ip.u4_size = sizeof(ivd_set_display_frame_ip_t); + s_set_display_frame_op.u4_size = sizeof(ivd_set_display_frame_op_t); + + cemu_assert_debug(s_ctl_op.u4_min_num_out_bufs == 2); + cemu_assert_debug(s_ctl_op.u4_min_out_buf_size[0] != 0 && s_ctl_op.u4_min_out_buf_size[1] != 0); + + s_set_display_frame_ip.num_disp_bufs = s_ctl_op.u4_num_disp_bufs; + + for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) + { + s_set_display_frame_ip.s_disp_buffer[i].u4_num_bufs = 2; + s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[0] = s_ctl_op.u4_min_out_buf_size[0]; + s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[1] = s_ctl_op.u4_min_out_buf_size[1]; + s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[0] = m_displayBuf[i].data() + 0; + s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[1] = m_displayBuf[i].data() + s_ctl_op.u4_min_out_buf_size[0]; + } + + status = ih264d_api_function(m_codecCtx, &s_set_display_frame_ip, &s_set_display_frame_op); + cemu_assert(!status); + + + // mark all as released (available) + for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) + { + ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; + ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; + + s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; + s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); + s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); + s_video_rel_disp_ip.u4_disp_buf_id = i; + + status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); + cemu_assert(!status); + } + } + + void ResetDecoder() + { + ivd_ctl_reset_ip_t s_ctl_ip; + ivd_ctl_reset_op_t s_ctl_op; + + s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_RESET; + s_ctl_ip.u4_size = sizeof(ivd_ctl_reset_ip_t); + s_ctl_op.u4_size = sizeof(ivd_ctl_reset_op_t); + + WORD32 status = ih264d_api_function(m_codecCtx, (void*)&s_ctl_ip, (void*)&s_ctl_op); + cemu_assert_debug(status == 0); + } + + void UpdateParameters(bool headerDecodeOnly) + { + ih264d_ctl_set_config_ip_t s_h264d_ctl_ip{ 0 }; + ih264d_ctl_set_config_op_t s_h264d_ctl_op{ 0 }; + ivd_ctl_set_config_ip_t* ps_ctl_ip = &s_h264d_ctl_ip.s_ivd_ctl_set_config_ip_t; + ivd_ctl_set_config_op_t* ps_ctl_op = &s_h264d_ctl_op.s_ivd_ctl_set_config_op_t; + + ps_ctl_ip->u4_disp_wd = 0; + ps_ctl_ip->e_frm_skip_mode = IVD_SKIP_NONE; + ps_ctl_ip->e_frm_out_mode = m_isBufferedMode ? IVD_DISPLAY_FRAME_OUT : IVD_DECODE_FRAME_OUT; + ps_ctl_ip->e_vid_dec_mode = headerDecodeOnly ? IVD_DECODE_HEADER : IVD_DECODE_FRAME; + ps_ctl_ip->e_cmd = IVD_CMD_VIDEO_CTL; + ps_ctl_ip->e_sub_cmd = IVD_CMD_CTL_SETPARAMS; + ps_ctl_ip->u4_size = sizeof(ih264d_ctl_set_config_ip_t); + ps_ctl_op->u4_size = sizeof(ih264d_ctl_set_config_op_t); + + WORD32 status = ih264d_api_function(m_codecCtx, &s_h264d_ctl_ip, &s_h264d_ctl_op); + cemu_assert(status == 0); + } + + private: + void DecoderThread() + { + while(!m_threadShouldExit) + { + m_decodeSem.decrementWithWait(); + std::unique_lock _l(m_decodeQueueMtx); + if (m_decodeQueue.empty()) + continue; + uint32 decodeIndex = m_decodeQueue.front(); + m_decodeQueue.erase(m_decodeQueue.begin()); + _l.unlock(); + if(decodeIndex == CMD_FLUSH) + { + Flush(); + _l.lock(); + cemu_assert_debug(m_decodeQueue.empty()); // after flushing the queue should be empty since the sender is waiting for the flush to complete + _l.unlock(); + coreinit::OSSignalEvent(m_flushEvt); + } + else + { + auto& decodedSlice = m_decodedSliceArray[decodeIndex]; + Decode(decodedSlice); + } + } + } + + iv_obj_t* m_codecCtx{nullptr}; + bool m_hasBufferSizeInfo{ false }; + bool m_isBufferedMode{ false }; + uint32 m_numDecodedFrames{0}; + std::vector> m_displayBuf; + + std::thread m_decoderThread; + std::atomic_bool m_threadShouldExit{false}; + }; + + H264DecoderBackend* CreateAVCDecoder() + { + return new H264AVCDecoder(); + } +}; diff --git a/src/Cafe/OS/libs/h264_avc/H264DecInternal.h b/src/Cafe/OS/libs/h264_avc/H264DecInternal.h new file mode 100644 index 00000000..498cccfa --- /dev/null +++ b/src/Cafe/OS/libs/h264_avc/H264DecInternal.h @@ -0,0 +1,139 @@ +#pragma once + +#include "util/helpers/Semaphore.h" +#include "Cafe/OS/libs/coreinit/coreinit_Thread.h" +#include "Cafe/OS/libs/coreinit/coreinit_SysHeap.h" + +#include "Cafe/OS/libs/h264_avc/parser/H264Parser.h" + +namespace H264 +{ + class H264DecoderBackend + { + protected: + struct DataToDecode + { + uint8* m_data; + uint32 m_length; + std::vector m_buffer; + }; + + static constexpr uint32 CMD_FLUSH = 0xFFFFFFFF; + + public: + struct DecodeResult + { + bool isDecoded{false}; + bool hasFrame{false}; // set to true if a full frame was successfully decoded + double timestamp{}; + void* imageOutput{nullptr}; + sint32 frameWidth{0}; + sint32 frameHeight{0}; + uint32 bytesPerRow{0}; + bool cropEnable{false}; + sint32 cropTop{0}; + sint32 cropBottom{0}; + sint32 cropLeft{0}; + sint32 cropRight{0}; + }; + + struct DecodedSlice + { + bool isUsed{false}; + DecodeResult result; + DataToDecode dataToDecode; + }; + + H264DecoderBackend() + { + m_displayQueueEvt = (coreinit::OSEvent*)coreinit::OSAllocFromSystem(sizeof(coreinit::OSEvent), 4); + coreinit::OSInitEvent(m_displayQueueEvt, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + m_flushEvt = (coreinit::OSEvent*)coreinit::OSAllocFromSystem(sizeof(coreinit::OSEvent), 4); + coreinit::OSInitEvent(m_flushEvt, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + }; + + virtual ~H264DecoderBackend() + { + coreinit::OSFreeToSystem(m_displayQueueEvt); + coreinit::OSFreeToSystem(m_flushEvt); + }; + + virtual void Init(bool isBufferedMode) = 0; + virtual void Destroy() = 0; + + void QueueForDecode(uint8* data, uint32 length, double timestamp, void* imagePtr) + { + std::unique_lock _l(m_decodeQueueMtx); + + DecodedSlice& ds = GetFreeDecodedSliceEntry(); + + ds.dataToDecode.m_buffer.assign(data, data + length); + ds.dataToDecode.m_data = ds.dataToDecode.m_buffer.data(); + ds.dataToDecode.m_length = length; + + ds.result.isDecoded = false; + ds.result.imageOutput = imagePtr; + ds.result.timestamp = timestamp; + + m_decodeQueue.push_back(std::distance(m_decodedSliceArray.data(), &ds)); + m_decodeSem.increment(); + } + + void QueueFlush() + { + std::unique_lock _l(m_decodeQueueMtx); + m_decodeQueue.push_back(CMD_FLUSH); + m_decodeSem.increment(); + } + + bool GetFrameOutputIfReady(DecodeResult& result) + { + std::unique_lock _l(m_decodeQueueMtx); + if(m_displayQueue.empty()) + return false; + uint32 sliceIndex = m_displayQueue.front(); + DecodedSlice& ds = m_decodedSliceArray[sliceIndex]; + cemu_assert_debug(ds.result.isDecoded); + std::swap(result, ds.result); + ds.isUsed = false; + m_displayQueue.erase(m_displayQueue.begin()); + return true; + } + + coreinit::OSEvent& GetFrameOutputEvent() + { + return *m_displayQueueEvt; + } + + coreinit::OSEvent& GetFlushEvent() + { + return *m_flushEvt; + } + + protected: + DecodedSlice& GetFreeDecodedSliceEntry() + { + for (auto& slice : m_decodedSliceArray) + { + if (!slice.isUsed) + { + slice.isUsed = true; + return slice; + } + } + cemu_assert_suspicious(); + return m_decodedSliceArray[0]; + } + + std::mutex m_decodeQueueMtx; + std::vector m_decodeQueue; // indices into m_decodedSliceArray, in order of decode input + CounterSemaphore m_decodeSem; + std::vector m_displayQueue; // indices into m_decodedSliceArray, in order of frame display output + coreinit::OSEvent* m_displayQueueEvt; // signalled when a new frame is ready for display + coreinit::OSEvent* m_flushEvt; // signalled after flush operation finished and all queued slices are decoded + + // frame output queue + std::mutex m_frameOutputMtx; + std::array m_decodedSliceArray; + }; +} \ No newline at end of file diff --git a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp index d77e551f..36f70f81 100644 --- a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp +++ b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp @@ -319,6 +319,17 @@ bool parseNAL_pic_parameter_set_rbsp(h264ParserState_t* h264ParserState, h264Par return true; } +bool h264Parser_ParseSPS(uint8* data, uint32 length, h264State_seq_parameter_set_t& sps) +{ + h264ParserState_t parserState; + RBSPInputBitstream nalStream(data, length); + bool r = parseNAL_seq_parameter_set_rbsp(&parserState, nullptr, nalStream); + if(!r || !parserState.hasSPS) + return false; + sps = parserState.sps; + return true; +} + void parseNAL_ref_pic_list_modification(const h264State_seq_parameter_set_t& sps, const h264State_pic_parameter_set_t& pps, RBSPInputBitstream& nalStream, nal_slice_header_t* sliceHeader) { if (!sliceHeader->slice_type.isSliceTypeI() && !sliceHeader->slice_type.isSliceTypeSI()) @@ -688,9 +699,8 @@ void _calculateFrameOrder(h264ParserState_t* h264ParserState, const h264State_se else if (sps.pic_order_cnt_type == 2) { // display order matches decode order - uint32 prevFrameNum = h264ParserState->picture_order.prevFrameNum; - ; + uint32 FrameNumOffset; if (sliceHeader->IdrPicFlag) { @@ -706,9 +716,6 @@ void _calculateFrameOrder(h264ParserState_t* h264ParserState, const h264State_se FrameNumOffset = prevFrameNumOffset + sps.getMaxFrameNum(); else FrameNumOffset = prevFrameNumOffset; - - - } uint32 tempPicOrderCnt; diff --git a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h index ee32ca8b..6f2b3cf6 100644 --- a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h +++ b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h @@ -513,6 +513,8 @@ typedef struct void h264Parse(h264ParserState_t* h264ParserState, h264ParserOutput_t* output, uint8* data, uint32 length, bool parseSlices = true); sint32 h264GetUnitLength(h264ParserState_t* h264ParserState, uint8* data, uint32 length); +bool h264Parser_ParseSPS(uint8* data, uint32 length, h264State_seq_parameter_set_t& sps); + void h264Parser_getScalingMatrix4x4(h264State_seq_parameter_set_t* sps, h264State_pic_parameter_set_t* pps, nal_slice_header_t* sliceHeader, sint32 index, uint8* matrix4x4); void h264Parser_getScalingMatrix8x8(h264State_seq_parameter_set_t* sps, h264State_pic_parameter_set_t* pps, nal_slice_header_t* sliceHeader, sint32 index, uint8* matrix8x8); From 026d547dccd568a67bd42214728b173811694a1e Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 01:45:34 +0200 Subject: [PATCH 37/73] Use HTTP 1.1 in Nintendo API requests --- src/Cemu/napi/napi_helper.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Cemu/napi/napi_helper.cpp b/src/Cemu/napi/napi_helper.cpp index 164de7e5..182c5371 100644 --- a/src/Cemu/napi/napi_helper.cpp +++ b/src/Cemu/napi/napi_helper.cpp @@ -107,6 +107,7 @@ CurlRequestHelper::CurlRequestHelper() curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(m_curl, CURLOPT_MAXREDIRS, 2); + curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); if(GetConfig().proxy_server.GetValue() != "") { @@ -263,6 +264,7 @@ CurlSOAPHelper::CurlSOAPHelper(NetworkService service) m_curl = curl_easy_init(); curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, __curlWriteCallback); curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, this); + curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // SSL if (!IsNetworkServiceSSLDisabled(service)) From 252429933f8ae8dde9443ee5cc2c17b83b7b9dc7 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 03:31:42 +0200 Subject: [PATCH 38/73] debugger: Slightly optimize symbol list updates --- src/gui/debugger/SymbolCtrl.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gui/debugger/SymbolCtrl.cpp b/src/gui/debugger/SymbolCtrl.cpp index cb1f3b1a..aa862987 100644 --- a/src/gui/debugger/SymbolCtrl.cpp +++ b/src/gui/debugger/SymbolCtrl.cpp @@ -46,25 +46,25 @@ SymbolListCtrl::SymbolListCtrl(wxWindow* parent, const wxWindowID& id, const wxP void SymbolListCtrl::OnGameLoaded() { m_data.clear(); - long itemId = 0; const auto symbol_map = rplSymbolStorage_lockSymbolMap(); for (auto const& [address, symbol_info] : symbol_map) { if (symbol_info == nullptr || symbol_info->symbolName == nullptr) continue; + wxString libNameWX = wxString::FromAscii((const char*)symbol_info->libName); + wxString symbolNameWX = wxString::FromAscii((const char*)symbol_info->symbolName); + wxString searchNameWX = libNameWX + symbolNameWX; + searchNameWX.MakeLower(); + auto new_entry = m_data.try_emplace( symbol_info->address, - (char*)(symbol_info->symbolName), - (char*)(symbol_info->libName), - "", + symbolNameWX, + libNameWX, + searchNameWX, false ); - new_entry.first->second.searchName += new_entry.first->second.name; - new_entry.first->second.searchName += new_entry.first->second.libName; - new_entry.first->second.searchName.MakeLower(); - if (m_list_filter.IsEmpty()) new_entry.first->second.visible = true; else if (new_entry.first->second.searchName.Contains(m_list_filter)) From 47f1dcf99691fbf0f8125f98f7df5ebf9eed221a Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:08:38 +0200 Subject: [PATCH 39/73] debugger: Add symbol support to PPC stack traces Also moved the declaration to precompiled.h instead of redefining it wherever it is used --- src/Cafe/HW/Espresso/Debugger/Debugger.cpp | 2 -- src/Cafe/OS/libs/coreinit/coreinit.cpp | 14 +++++++++++--- src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp | 2 -- src/Common/ExceptionHandler/ExceptionHandler.cpp | 4 +--- src/Common/precompiled.h | 3 +++ .../PPCThreadsViewer/DebugPPCThreadsWindow.cpp | 4 +--- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp index 62a5d592..e7369af6 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp @@ -501,8 +501,6 @@ void debugger_createPPCStateSnapshot(PPCInterpreter_t* hCPU) debuggerState.debugSession.ppcSnapshot.cr[i] = hCPU->cr[i]; } -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - void debugger_enterTW(PPCInterpreter_t* hCPU) { // handle logging points diff --git a/src/Cafe/OS/libs/coreinit/coreinit.cpp b/src/Cafe/OS/libs/coreinit/coreinit.cpp index 49d232f8..00327a97 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit.cpp @@ -1,6 +1,6 @@ #include "Cafe/OS/common/OSCommon.h" #include "Common/SysAllocator.h" -#include "Cafe/OS/RPL/rpl.h" +#include "Cafe/OS/RPL/rpl_symbol_storage.h" #include "Cafe/OS/libs/coreinit/coreinit_Misc.h" @@ -69,7 +69,7 @@ sint32 ScoreStackTrace(OSThread_t* thread, MPTR sp) return score; } -void DebugLogStackTrace(OSThread_t* thread, MPTR sp) +void DebugLogStackTrace(OSThread_t* thread, MPTR sp, bool printSymbols) { // sp might not point to a valid stackframe // scan stack and evaluate which sp is most likely the beginning of the stackframe @@ -107,7 +107,15 @@ void DebugLogStackTrace(OSThread_t* thread, MPTR sp) uint32 returnAddress = 0; returnAddress = memory_readU32(nextStackPtr + 4); - cemuLog_log(LogType::Force, fmt::format("SP {0:08x} ReturnAddr {1:08x}", nextStackPtr, returnAddress)); + + RPLStoredSymbol* symbol = nullptr; + if(printSymbols) + symbol = rplSymbolStorage_getByClosestAddress(returnAddress); + + if(symbol) + cemuLog_log(LogType::Force, fmt::format("SP {:08x} ReturnAddr {:08x} ({}.{}+0x{:x})", nextStackPtr, returnAddress, (const char*)symbol->libName, (const char*)symbol->symbolName, returnAddress - symbol->address)); + else + cemuLog_log(LogType::Force, fmt::format("SP {:08x} ReturnAddr {:08x}", nextStackPtr, returnAddress)); currentStackPtr = nextStackPtr; } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp b/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp index 7ddadcf1..552a610f 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp @@ -2,8 +2,6 @@ #include "Cafe/HW/Espresso/PPCCallback.h" #include "Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.h" -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - #define EXP_HEAP_GET_FROM_FREE_BLOCKCHAIN(__blockchain__) (MEMExpHeapHead2*)((uintptr_t)__blockchain__ - offsetof(MEMExpHeapHead2, expHeapHead) - offsetof(MEMExpHeapHead40_t, chainFreeBlocks)) namespace coreinit diff --git a/src/Common/ExceptionHandler/ExceptionHandler.cpp b/src/Common/ExceptionHandler/ExceptionHandler.cpp index b6755fd8..7530a2eb 100644 --- a/src/Common/ExceptionHandler/ExceptionHandler.cpp +++ b/src/Common/ExceptionHandler/ExceptionHandler.cpp @@ -6,8 +6,6 @@ #include "Cafe/HW/Espresso/Debugger/GDBStub.h" #include "ExceptionHandler.h" -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - bool crashLogCreated = false; bool CrashLog_Create() @@ -97,7 +95,7 @@ void ExceptionHandler_LogGeneralInfo() MPTR currentStackVAddr = hCPU->gpr[1]; CrashLog_WriteLine(""); CrashLog_WriteHeader("PPC stack trace"); - DebugLogStackTrace(currentThread, currentStackVAddr); + DebugLogStackTrace(currentThread, currentStackVAddr, true); // stack dump CrashLog_WriteLine(""); diff --git a/src/Common/precompiled.h b/src/Common/precompiled.h index 790a001a..61707519 100644 --- a/src/Common/precompiled.h +++ b/src/Common/precompiled.h @@ -552,6 +552,9 @@ inline uint32 GetTitleIdLow(uint64 titleId) #include "Cafe/HW/Espresso/PPCState.h" #include "Cafe/HW/Espresso/PPCCallback.h" +// PPC stack trace printer +void DebugLogStackTrace(struct OSThread_t* thread, MPTR sp, bool printSymbols = false); + // generic formatter for enums (to underlying) template requires std::is_enum_v diff --git a/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp b/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp index dfbaf76e..f4e5b7af 100644 --- a/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp +++ b/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp @@ -277,12 +277,10 @@ void DebugPPCThreadsWindow::RefreshThreadList() m_thread_list->SetScrollPos(0, scrollPos, true); } -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - void DebugPPCThreadsWindow::DumpStackTrace(OSThread_t* thread) { cemuLog_log(LogType::Force, "Dumping stack trace for thread {0:08x} LR: {1:08x}", memory_getVirtualOffsetFromPointer(thread), _swapEndianU32(thread->context.lr)); - DebugLogStackTrace(thread, _swapEndianU32(thread->context.gpr[1])); + DebugLogStackTrace(thread, _swapEndianU32(thread->context.gpr[1]), true); } void DebugPPCThreadsWindow::PresentProfileResults(OSThread_t* thread, const std::unordered_map& samples) From 5328e9eb10b2abeaf303310b06b37269d79dde12 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:13:45 +0200 Subject: [PATCH 40/73] CPU: Fix overflow bit calculation in SUBFO instruction Since rD can overlap with rA or rB the result needs to be stored in a temporary --- src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp index ed97288d..fe9316f0 100644 --- a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp +++ b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp @@ -212,11 +212,12 @@ static void PPCInterpreter_SUBF(PPCInterpreter_t* hCPU, uint32 opcode) static void PPCInterpreter_SUBFO(PPCInterpreter_t* hCPU, uint32 opcode) { - // untested (Don't Starve Giant Edition uses this) + // Seen in Don't Starve Giant Edition and Teslagrad // also used by DS Virtual Console (Super Mario 64 DS) PPC_OPC_TEMPL3_XO(); - hCPU->gpr[rD] = ~hCPU->gpr[rA] + hCPU->gpr[rB] + 1; - PPCInterpreter_setXerOV(hCPU, checkAdditionOverflow(~hCPU->gpr[rA], hCPU->gpr[rB], hCPU->gpr[rD])); + uint32 result = ~hCPU->gpr[rA] + hCPU->gpr[rB] + 1; + PPCInterpreter_setXerOV(hCPU, checkAdditionOverflow(~hCPU->gpr[rA], hCPU->gpr[rB], result)); + hCPU->gpr[rD] = result; if (opHasRC()) ppc_update_cr0(hCPU, hCPU->gpr[rD]); PPCInterpreter_nextInstruction(hCPU); From c73fa3761c9572db4d09cdb976a0f1510cda548a Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:45:36 +0200 Subject: [PATCH 41/73] Fix compatibility with GCC --- src/resource/embedded/fontawesome.S | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resource/embedded/fontawesome.S b/src/resource/embedded/fontawesome.S index 29b4f93a..db23e7ae 100644 --- a/src/resource/embedded/fontawesome.S +++ b/src/resource/embedded/fontawesome.S @@ -1,4 +1,4 @@ -.rodata +.section .rodata,"",%progbits .global g_fontawesome_data, g_fontawesome_size g_fontawesome_data: @@ -6,3 +6,4 @@ g_fontawesome_data: g_fontawesome_size: .int g_fontawesome_size - g_fontawesome_data +.section .note.GNU-stack,"",%progbits \ No newline at end of file From 593da5ed79cab9ca175391d0ba6666a1f8a52500 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 27 Jul 2024 18:33:01 +0200 Subject: [PATCH 42/73] CI: Workaround for MoltenVK crash 1.2.10 and later crash during descriptor set creation. So for now let's stick with the older version --- .github/workflows/build.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2342c27..28efa833 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -239,7 +239,17 @@ jobs: - name: "Install system dependencies" run: | brew update - brew install llvm@15 ninja nasm molten-vk automake libtool + brew install llvm@15 ninja nasm automake libtool + brew install cmake python3 ninja + + - name: "Build and install molten-vk" + run: | + git clone https://github.com/KhronosGroup/MoltenVK.git + cd MoltenVK + git checkout bf097edc74ec3b6dfafdcd5a38d3ce14b11952d6 + ./fetchDependencies --macos + make macos + make install - name: "Setup cmake" uses: jwlawson/actions-setup-cmake@v2 From 517e68fe57ac1ac112f37c321d3d40a30ea5a8d6 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Sun, 28 Jul 2024 17:50:20 +0100 Subject: [PATCH 43/73] nsyshid: Tidyups and Fixes (#1275) --- src/Cafe/OS/libs/nsyshid/Skylander.cpp | 2 +- src/Cafe/OS/libs/nsyshid/Skylander.h | 2 +- src/config/CemuConfig.h | 2 +- src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp index a9888787..1b4515ef 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.cpp +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -978,7 +978,7 @@ namespace nsyshid { for (const auto& it : GetListSkylanders()) { - if(it.first.first == skyId && it.first.second == skyVar) + if (it.first.first == skyId && it.first.second == skyVar) { return it.second; } diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h index 95eaff0c..986ef185 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.h +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -50,7 +50,7 @@ namespace nsyshid std::unique_ptr skyFile; uint8 status = 0; std::queue queuedStatus; - std::array data{}; + std::array data{}; uint32 lastId = 0; void Save(); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 2a1d29cb..ac861c9a 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -519,7 +519,7 @@ struct CemuConfig struct { ConfigValue emulate_skylander_portal{false}; - ConfigValue emulate_infinity_base{true}; + ConfigValue emulate_infinity_base{false}; }emulated_usb_devices{}; private: diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index f4784f35..3a0f534a 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -398,7 +398,7 @@ CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 s { wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); idError.ShowModal(); - this->EndModal(0);; + this->EndModal(0); } uint32 figNum = longFigNum & 0xFFFFFFFF; auto figure = nsyshid::g_infinitybase.FindFigure(figNum); @@ -408,7 +408,7 @@ CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 s "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (saveFileDialog.ShowModal() == wxID_CANCEL) - this->EndModal(0);; + this->EndModal(0); m_filePath = saveFileDialog.GetPath(); From 1575866eca8f84bbe94f2e3a5c2bc8938a5856bb Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:45:57 +0200 Subject: [PATCH 44/73] Vulkan: Add R32_X8_FLOAT format --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 9209e3cd..b9922fc3 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2439,6 +2439,11 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD // used by Color Splash and Resident Evil formatInfoOut->vkImageFormat = VK_FORMAT_R8G8B8A8_UINT; // todo - should we use ABGR format? formatInfoOut->decoder = TextureDecoder_X24_G8_UINT::getInstance(); // todo - verify + case Latte::E_GX2SURFFMT::R32_X8_FLOAT: + // seen in Disney Infinity 3.0 + formatInfoOut->vkImageFormat = VK_FORMAT_R32_SFLOAT; + formatInfoOut->decoder = TextureDecoder_NullData64::getInstance(); + break; default: cemuLog_log(LogType::Force, "Unsupported color texture format {:04x}", (uint32)format); cemu_assert_debug(false); From d81eb952a4c8273670c9a29fc3c7e69961282d41 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:58:23 +0200 Subject: [PATCH 45/73] nsyshid: Silence some logging in release builds --- src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp index 44e01399..267111b2 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp @@ -67,13 +67,6 @@ namespace nsyshid::backend::windows device->m_productId); } } - else - { - cemuLog_log(LogType::Force, - "nsyshid::BackendWindowsHID: device not on whitelist: {:04x}:{:04x}", - device->m_vendorId, - device->m_productId); - } } CloseHandle(hHIDDevice); } @@ -125,14 +118,12 @@ namespace nsyshid::backend::windows } if (maxPacketInputLength <= 0 || maxPacketInputLength >= 0xF000) { - cemuLog_log(LogType::Force, "HID: Input packet length not available or out of range (length = {})", - maxPacketInputLength); + cemuLog_logDebug(LogType::Force, "HID: Input packet length not available or out of range (length = {})", maxPacketInputLength); maxPacketInputLength = 0x20; } if (maxPacketOutputLength <= 0 || maxPacketOutputLength >= 0xF000) { - cemuLog_log(LogType::Force, "HID: Output packet length not available or out of range (length = {})", - maxPacketOutputLength); + cemuLog_logDebug(LogType::Force, "HID: Output packet length not available or out of range (length = {})", maxPacketOutputLength); maxPacketOutputLength = 0x20; } From 21296447812794f8cde76db93cf3697a96da7ac9 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:02:28 +0200 Subject: [PATCH 46/73] Remove shaderCache directory The location of the shaderCache path is different for non-portable cases so let's not confuse the user by shipping with a precreated directory that isn't actually used --- bin/shaderCache/info.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bin/shaderCache/info.txt diff --git a/bin/shaderCache/info.txt b/bin/shaderCache/info.txt deleted file mode 100644 index 962cf88b..00000000 --- a/bin/shaderCache/info.txt +++ /dev/null @@ -1 +0,0 @@ -If you plan to transfer the shader cache to a different PC or Cemu installation you only need to copy the 'transferable' directory. \ No newline at end of file From b52b676413a5566adc4a5c1f2472fa6cc961a94c Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 7 Aug 2024 02:50:24 +0200 Subject: [PATCH 47/73] vcpkg: Automatically unshallow submodule --- CMakeLists.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b5f3881..48e18637 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,24 @@ if (EXPERIMENTAL_VERSION) endif() if (ENABLE_VCPKG) + # check if vcpkg is shallow and unshallow it if necessary + execute_process( + COMMAND git rev-parse --is-shallow-repository + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/dependencies/vcpkg + OUTPUT_VARIABLE is_vcpkg_shallow + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(is_vcpkg_shallow STREQUAL "true") + message(STATUS "vcpkg is shallow. Unshallowing it now...") + execute_process( + COMMAND git fetch --unshallow + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/dependencies/vcpkg" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ) + endif() + if(UNIX AND NOT APPLE) set(VCPKG_OVERLAY_PORTS "${CMAKE_CURRENT_LIST_DIR}/dependencies/vcpkg_overlay_ports_linux") elseif(APPLE) From bf2208145b21505f5ebe3b1245e4c32bfc6f0d45 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:18:40 +0200 Subject: [PATCH 48/73] Enable async shader compile by default --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 4 +++- src/config/CemuConfig.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index b9922fc3..09515993 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -7,6 +7,7 @@ #include "Cafe/HW/Latte/Core/LatteBufferCache.h" #include "Cafe/HW/Latte/Core/LattePerformanceMonitor.h" +#include "Cafe/HW/Latte/Core/LatteOverlay.h" #include "Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.h" @@ -29,6 +30,7 @@ #include #include +#include // for localization #ifndef VK_API_VERSION_MAJOR #define VK_API_VERSION_MAJOR(version) (((uint32_t)(version) >> 22) & 0x7FU) @@ -285,7 +287,7 @@ void VulkanRenderer::GetDeviceFeatures() cemuLog_log(LogType::Force, "VK_EXT_pipeline_creation_cache_control not supported. Cannot use asynchronous shader and pipeline compilation"); // if async shader compilation is enabled show warning message if (GetConfig().async_compile) - wxMessageBox(_("The currently installed graphics driver does not support the Vulkan extension necessary for asynchronous shader compilation. Asynchronous compilation cannot be used.\n \nRequired extension: VK_EXT_pipeline_creation_cache_control\n\nInstalling the latest graphics driver may solve this error."), _("Information"), wxOK | wxCENTRE); + LatteOverlay_pushNotification(_("Async shader compile is enabled but not supported by the graphics driver\nCemu will use synchronous compilation which can cause additional stutter").utf8_string(), 10000); } if (!m_featureControl.deviceExtensions.custom_border_color_without_format) { diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index ac861c9a..5db8f58c 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -441,7 +441,7 @@ struct CemuConfig ConfigValue vsync{ 0 }; // 0 = off, 1+ = on depending on render backend ConfigValue gx2drawdone_sync {true}; ConfigValue render_upside_down{ false }; - ConfigValue async_compile{ false }; + ConfigValue async_compile{ true }; ConfigValue vk_accurate_barriers{ true }; From 54e695a6e81efd4fb9a632c62abdb4585f8f776e Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:58:24 +0200 Subject: [PATCH 49/73] git: unshallow vcpkg, shallow vulkan-headers and imgui (#1282) --- .gitmodules | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index f352d478..dc69c441 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,10 +9,12 @@ [submodule "dependencies/vcpkg"] path = dependencies/vcpkg url = https://github.com/microsoft/vcpkg - shallow = true + shallow = false [submodule "dependencies/Vulkan-Headers"] path = dependencies/Vulkan-Headers url = https://github.com/KhronosGroup/Vulkan-Headers + shallow = true [submodule "dependencies/imgui"] path = dependencies/imgui url = https://github.com/ocornut/imgui + shallow = true From 598298cb3d28fd608878f13ef1e76add75173692 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:07:08 +0200 Subject: [PATCH 50/73] Vulkan: Fix stencil front mask --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp index ce582b9a..ba094a84 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp @@ -826,7 +826,7 @@ void PipelineCompiler::InitDepthStencilState() depthStencilState.front.reference = stencilRefFront; depthStencilState.front.compareMask = stencilCompareMaskFront; - depthStencilState.front.writeMask = stencilWriteMaskBack; + depthStencilState.front.writeMask = stencilWriteMaskFront; depthStencilState.front.compareOp = vkDepthCompareTable[(size_t)frontStencilFunc]; depthStencilState.front.depthFailOp = stencilOpTable[(size_t)frontStencilZFail]; depthStencilState.front.failOp = stencilOpTable[(size_t)frontStencilFail]; From 7fd532436d5af65af5a27a532d7ea5cb6ac5895c Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:07:36 +0200 Subject: [PATCH 51/73] CI: Manual unshallow of vcpkg is no longer needed --- .github/workflows/build.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28efa833..015ef367 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,11 +24,6 @@ jobs: submodules: "recursive" fetch-depth: 0 - - name: "Fetch full history for vcpkg submodule" - run: | - cd dependencies/vcpkg - git fetch --unshallow - - name: Setup release mode parameters (for deploy) if: ${{ inputs.deploymode == 'release' }} run: | @@ -133,11 +128,6 @@ jobs: with: submodules: "recursive" - - name: "Fetch full history for vcpkg submodule" - run: | - cd dependencies/vcpkg - git fetch --unshallow - - name: Setup release mode parameters (for deploy) if: ${{ inputs.deploymode == 'release' }} run: | @@ -212,11 +202,6 @@ jobs: with: submodules: "recursive" - - name: "Fetch full history for vcpkg submodule" - run: | - cd dependencies/vcpkg - git fetch --unshallow - - name: Setup release mode parameters (for deploy) if: ${{ inputs.deploymode == 'release' }} run: | From 9812a47cb182331f7c7c7a6a16eff014098a6206 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:35:50 +0200 Subject: [PATCH 52/73] clang-format: Put class braces on a new line (#1283) --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index b22a1048..0cef9ae4 100644 --- a/.clang-format +++ b/.clang-format @@ -15,6 +15,7 @@ BinPackArguments: true BinPackParameters: true BraceWrapping: AfterCaseLabel: true + AfterClass: true AfterControlStatement: Always AfterEnum: true AfterExternBlock: true 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 53/73] 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 54/73] 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 55/73] 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 56/73] 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 57/73] 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 58/73] 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 59/73] 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 60/73] 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 61/73] 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 62/73] 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 63/73] 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 64/73] 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 65/73] 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 66/73] 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 67/73] 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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