diff --git a/src/Cafe/GraphicPack/GraphicPack2.h b/src/Cafe/GraphicPack/GraphicPack2.h index e332a5a7..594f1e5c 100644 --- a/src/Cafe/GraphicPack/GraphicPack2.h +++ b/src/Cafe/GraphicPack/GraphicPack2.h @@ -278,6 +278,7 @@ private: void ParseCemuhookPatchesTxtInternal(MemStreamReader& patchesStream); bool ParseCemuPatchesTxtInternal(MemStreamReader& patchesStream); + bool ParseCemuBinaryPatchesInternal(MemStreamReader& patchesStream); void CancelParsingPatches(); void ApplyPatchGroups(std::vector& groups, const RPLModule* rpl); diff --git a/src/Cafe/GraphicPack/GraphicPack2Patches.cpp b/src/Cafe/GraphicPack/GraphicPack2Patches.cpp index 7ceaf9d4..fe6778b0 100644 --- a/src/Cafe/GraphicPack/GraphicPack2Patches.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2Patches.cpp @@ -66,56 +66,66 @@ void PatchErrorHandler::showStageErrorMessageBox() WindowSystem::ShowErrorDialog(errorMsg, _tr("Graphic pack error"), WindowSystem::ErrorCategory::GRAPHIC_PACKS); } -// loads Cemu-style patches (patch_.asm) +// loads Cemu-style patches (patch_.cpb or patch_.asm) // returns true if at least one file was found even if it could not be successfully parsed bool GraphicPack2::LoadCemuPatches() { - bool foundPatches = false; fs::path path(m_rulesPath); path.remove_filename(); - for (auto& p : fs::directory_iterator(path)) + + auto loadPatchFilesByExtension = [&](const char* extension, bool isBinaryPatch) -> bool { - auto& path = p.path(); - if (fs::is_regular_file(p.status()) && path.has_filename()) + bool foundPatches = false; + for (auto& p : fs::directory_iterator(path)) { - // check if filename matches - std::string filename = _pathToUtf8(path.filename()); - if (boost::istarts_with(filename, "patch_") && boost::iends_with(filename, ".asm")) + auto& patchPath = p.path(); + if (fs::is_regular_file(p.status()) && patchPath.has_filename()) { - FileStream* patchFile = FileStream::openFile2(path); - if (patchFile) + // check if filename matches + std::string filename = _pathToUtf8(patchPath.filename()); + if (boost::istarts_with(filename, "patch_") && boost::iends_with(filename, extension)) { - // read file - std::vector fileData; - patchFile->extract(fileData); - delete patchFile; - MemStreamReader patchesStream(fileData.data(), (sint32)fileData.size()); - // load Cemu style patch file - if (!ParseCemuPatchesTxtInternal(patchesStream)) + FileStream* patchFile = FileStream::openFile2(patchPath); + if (patchFile) { - cemuLog_log(LogType::Force, "Error while processing \"{}\". No patches for this graphic pack will be applied.", _pathToUtf8(path)); - cemu_assert_debug(list_patchGroups.empty()); - return true; // return true since a .asm patch was found even if we could not parse it + // read file + std::vector fileData; + patchFile->extract(fileData); + delete patchFile; + MemStreamReader patchesStream(fileData.data(), (sint32)fileData.size()); + // load Cemu style patch file + const bool parseResult = isBinaryPatch ? ParseCemuBinaryPatchesInternal(patchesStream) : ParseCemuPatchesTxtInternal(patchesStream); + if (!parseResult) + { + cemuLog_log(LogType::Force, "Error while processing \"{}\". No patches for this graphic pack will be applied.", _pathToUtf8(patchPath)); + cemu_assert_debug(list_patchGroups.empty()); + return true; // return true since a patch was found even if we could not parse it + } } + else + { + cemuLog_log(LogType::Force, "Unable to load patch file \"{}\"", _pathToUtf8(patchPath)); + } + foundPatches = true; } - else - { - cemuLog_log(LogType::Force, "Unable to load patch file \"{}\"", _pathToUtf8(path)); - } - foundPatches = true; } } - } - return foundPatches; + return foundPatches; + }; + + if (loadPatchFilesByExtension(".cpb", true)) + return true; + return loadPatchFilesByExtension(".asm", false); } void GraphicPack2::LoadPatchFiles() { // order of loading patches: - // 1) Load Cemu-style patches (patch_.asm), stop here if at least one patch file exists - // 2) Load Cemuhook patches.txt + // 1) Load Cemu binary patches (patch_.cpb), stop here if at least one patch file exists + // 2) Load Cemu-style patches (patch_.asm), stop here if at least one patch file exists + // 3) Load Cemuhook patches.txt if (LoadCemuPatches()) - return; // exit if at least one Cemu style patch file was found + return; // exit if at least one Cemu patch file was found // fall back to Cemuhook patches.txt to guarantee backward compatibility fs::path path(m_rulesPath); path.remove_filename(); diff --git a/src/Cafe/GraphicPack/GraphicPack2PatchesParser.cpp b/src/Cafe/GraphicPack/GraphicPack2PatchesParser.cpp index d93cc151..fba86833 100644 --- a/src/Cafe/GraphicPack/GraphicPack2PatchesParser.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2PatchesParser.cpp @@ -33,6 +33,133 @@ void GraphicPack2::LogPatchesSyntaxError(sint32 lineNumber, std::string_view err list_patchGroups.clear(); } +enum class RAW_HEX_PARSE_RESULT +{ + NO_MATCH, + MATCH, + PARSE_ERROR +}; + +static constexpr uint32 CEMU_PATCH_BINARY_MAGIC = 0x43504231; // CPB1 +static constexpr uint8 CEMU_PATCH_BINARY_ENTRY_LABEL = 1; +static constexpr uint8 CEMU_PATCH_BINARY_ENTRY_DATA = 2; + +static bool _isHexDigit(char c) +{ + return (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + +static uint8 _hexDigitToValue(char c) +{ + if (c >= '0' && c <= '9') + return (uint8)(c - '0'); + if (c >= 'a' && c <= 'f') + return (uint8)(c - 'a' + 10); + return (uint8)(c - 'A' + 10); +} + +static RAW_HEX_PARSE_RESULT _parseRawHexPatchData(std::string_view text, std::vector& outData, std::string& errorMsg) +{ + auto isWhitespace = [](char c) { return c == ' ' || c == '\t'; }; + size_t index = 0; + bool parsedAnyValue = false; + while (index < text.size()) + { + while (index < text.size() && (isWhitespace(text[index]) || text[index] == ',')) + index++; + if (index >= text.size()) + break; + if (index + 2 > text.size() || text[index] != '0' || (text[index + 1] != 'x' && text[index + 1] != 'X')) + { + if (parsedAnyValue) + errorMsg = "Unexpected characters in raw binary patch data"; + return parsedAnyValue ? RAW_HEX_PARSE_RESULT::PARSE_ERROR : RAW_HEX_PARSE_RESULT::NO_MATCH; + } + index += 2; + size_t digitStart = index; + uint32 value = 0; + while (index < text.size() && _isHexDigit(text[index])) + { + if ((index - digitStart) >= 8) + { + errorMsg = "Raw binary patch values must fit in 32 bits"; + return RAW_HEX_PARSE_RESULT::PARSE_ERROR; + } + value = (value << 4) | _hexDigitToValue(text[index]); + index++; + } + size_t digitCount = index - digitStart; + if (digitCount == 0) + { + errorMsg = "Expected hexadecimal digits after 0x"; + return RAW_HEX_PARSE_RESULT::PARSE_ERROR; + } + uint32 byteCount = (uint32)((digitCount + 1) / 2); + for (sint32 byteIndex = (sint32)byteCount - 1; byteIndex >= 0; byteIndex--) + outData.emplace_back((uint8)((value >> (byteIndex * 8)) & 0xFF)); + parsedAnyValue = true; + if (index < text.size() && !isWhitespace(text[index]) && text[index] != ',') + { + errorMsg = "Unexpected characters after raw binary patch value"; + return RAW_HEX_PARSE_RESULT::PARSE_ERROR; + } + } + return parsedAnyValue ? RAW_HEX_PARSE_RESULT::MATCH : RAW_HEX_PARSE_RESULT::NO_MATCH; +} + +static bool _isValidBinaryRelocType(uint8 relocType) +{ + switch ((PPCASM_RELOC)relocType) + { + case PPCASM_RELOC::U32_MASKED_IMM: + case PPCASM_RELOC::BRANCH_S16: + case PPCASM_RELOC::BRANCH_S26: + case PPCASM_RELOC::FLOAT: + case PPCASM_RELOC::DOUBLE: + case PPCASM_RELOC::U32: + case PPCASM_RELOC::U16: + case PPCASM_RELOC::U8: + return true; + default: + return false; + } +} + +static uint32 _getBinaryRelocSize(PPCASM_RELOC relocType) +{ + switch (relocType) + { + case PPCASM_RELOC::DOUBLE: + return sizeof(betype); + case PPCASM_RELOC::U16: + return sizeof(betype); + case PPCASM_RELOC::U8: + return sizeof(betype); + case PPCASM_RELOC::U32_MASKED_IMM: + case PPCASM_RELOC::BRANCH_S16: + case PPCASM_RELOC::BRANCH_S26: + case PPCASM_RELOC::FLOAT: + case PPCASM_RELOC::U32: + return sizeof(betype); + default: + return 0; + } +} + +static bool _readBinaryPatchString(MemStreamReader& patchesStream, std::string& str) +{ + uint16 strLength = patchesStream.readBE(); + if (patchesStream.hasError()) + return false; + auto strData = patchesStream.readDataNoCopy(strLength); + if (patchesStream.hasError()) + return false; + str.assign((const char*)strData.data(), strData.size()); + return true; +} + void GraphicPack2::CancelParsingPatches() { // unload everything, set error flag @@ -50,6 +177,7 @@ void GraphicPack2::AddPatchGroup(PatchGroup* group) } // calculate code cave size uint32 codeCaveMaxAddr = 0; + uint32 codeCavePatchedBytes = 0; for (auto& itr : group->list_patches) { PatchEntryInstruction* patchData = dynamic_cast(itr); @@ -60,12 +188,12 @@ void GraphicPack2::AddPatchGroup(PatchGroup* group) { // everything in low 1MB of memory we consider part of the code cave codeCaveMaxAddr = std::max(codeCaveMaxAddr, patchAddr + patchData->getSize()); + codeCavePatchedBytes += patchData->getSize(); } } } - uint32 numEstimatedCodeCaveInstr = codeCaveMaxAddr / 4; - if (group->list_patches.size() < (numEstimatedCodeCaveInstr / 8)) + if (codeCavePatchedBytes < (codeCaveMaxAddr / 8)) { // if less than 1/8th of the code cave is filled print a warning cemuLog_log(LogType::Force, "Graphic pack patches: Code cave for group [{}] in gfx pack \"{}\" ranges from 0 to 0x{:x} but has only few instructions. Is this intentional?", group->name, this->m_name, codeCaveMaxAddr); @@ -141,8 +269,30 @@ void GraphicPack2::ParseCemuhookPatchesTxtInternal(MemStreamReader& patchesStrea } parser.skipWhitespaces(); parser.trimWhitespaces(); - // assemble instruction + // raw binary patch data std::string instrText(parser.getCurrentPtr(), parser.getCurrentLen()); + std::vector rawPatchData; + std::string rawPatchError; + RAW_HEX_PARSE_RESULT rawPatchResult = _parseRawHexPatchData(instrText, rawPatchData, rawPatchError); + if (rawPatchResult == RAW_HEX_PARSE_RESULT::PARSE_ERROR) + { + LogPatchesSyntaxError(lineNumber, rawPatchError); + CancelParsingPatches(); + return; + } + if (rawPatchResult == RAW_HEX_PARSE_RESULT::MATCH) + { + if (currentGroup == nullptr) + { + LogPatchesSyntaxError(lineNumber, "Raw binary patch data specified outside of a group"); + CancelParsingPatches(); + return; + } + std::vector noRelocs; + currentGroup->list_patches.emplace_back(new PatchEntryInstruction(lineNumber, patchedAddress, { rawPatchData.data(), rawPatchData.size() }, noRelocs)); + continue; + } + // assemble instruction PPCAssemblerInOut ctx{}; ctx.virtualAddress = patchedAddress; if (!ppcAssembler_assembleSingleInstruction(instrText.c_str(), &ctx)) @@ -223,6 +373,176 @@ void GraphicPack2::ParseCemuhookPatchesTxtInternal(MemStreamReader& patchesStrea AddPatchGroup(currentGroup); } +bool GraphicPack2::ParseCemuBinaryPatchesInternal(MemStreamReader& patchesStream) +{ + uint32 magic = patchesStream.readBE(); + if (patchesStream.hasError() || magic != CEMU_PATCH_BINARY_MAGIC) + { + LogPatchesSyntaxError(-1, "Invalid binary patch file header"); + CancelParsingPatches(); + return false; + } + + uint32 groupCount = patchesStream.readBE(); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Unexpected end of binary patch file"); + CancelParsingPatches(); + return false; + } + + for (uint32 groupIndex = 0; groupIndex < groupCount; groupIndex++) + { + std::string groupName; + if (!_readBinaryPatchString(patchesStream, groupName) || groupName.empty()) + { + LogPatchesSyntaxError(-1, "Invalid group name in binary patch file"); + CancelParsingPatches(); + return false; + } + + PatchGroup* currentGroup = new PatchGroup(this, groupName.data(), (sint32)groupName.size()); + + uint32 moduleMatchCount = patchesStream.readBE(); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Unexpected end of binary patch file while reading moduleMatches"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + if (moduleMatchCount == 0) + { + LogPatchesSyntaxError(-1, "Binary patch group has no moduleMatches definition"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + for (uint32 moduleMatchIndex = 0; moduleMatchIndex < moduleMatchCount; moduleMatchIndex++) + { + currentGroup->list_moduleMatches.emplace_back(patchesStream.readBE()); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Unexpected end of binary patch file while reading moduleMatches"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + } + + uint32 entryCount = patchesStream.readBE(); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Unexpected end of binary patch file while reading entries"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + + for (uint32 entryIndex = 0; entryIndex < entryCount; entryIndex++) + { + uint8 entryType = patchesStream.readBE(); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Unexpected end of binary patch file while reading entry type"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + + if (entryType == CEMU_PATCH_BINARY_ENTRY_LABEL) + { + uint32 labelAddress = patchesStream.readBE(); + std::string labelName; + if (!_readBinaryPatchString(patchesStream, labelName) || labelName.empty()) + { + LogPatchesSyntaxError(-1, "Invalid label entry in binary patch file"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + PatchEntryLabel* patchLabel = new PatchEntryLabel((sint32)entryIndex + 1, labelName.data(), (sint32)labelName.size()); + patchLabel->setAssignedVA(labelAddress); + currentGroup->list_patches.emplace_back(patchLabel); + } + else if (entryType == CEMU_PATCH_BINARY_ENTRY_DATA) + { + uint32 patchAddress = patchesStream.readBE(); + uint32 dataSize = patchesStream.readBE(); + uint32 relocCount = patchesStream.readBE(); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Invalid data entry header in binary patch file"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + auto patchData = patchesStream.readDataNoCopy(dataSize); + if (patchesStream.hasError()) + { + LogPatchesSyntaxError(-1, "Unexpected end of binary patch file while reading patch data"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + std::vector relocs; + for (uint32 relocIndex = 0; relocIndex < relocCount; relocIndex++) + { + uint8 relocTypeRaw = patchesStream.readBE(); + uint32 byteOffset = patchesStream.readBE(); + uint8 bitOffset = patchesStream.readBE(); + uint8 bitCount = patchesStream.readBE(); + std::string expression; + if (patchesStream.hasError() || !_isValidBinaryRelocType(relocTypeRaw) || !_readBinaryPatchString(patchesStream, expression) || expression.empty()) + { + LogPatchesSyntaxError(-1, "Invalid relocation entry in binary patch file"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + + PPCASM_RELOC relocType = (PPCASM_RELOC)relocTypeRaw; + uint32 relocSize = _getBinaryRelocSize(relocType); + if (byteOffset > dataSize || relocSize > (dataSize - byteOffset)) + { + LogPatchesSyntaxError(-1, "Relocation range is outside of patch data"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + if (relocType == PPCASM_RELOC::U32_MASKED_IMM && (bitCount == 0 || bitCount > 32 || bitOffset >= 32 || ((uint32)bitOffset + bitCount) > 32)) + { + LogPatchesSyntaxError(-1, "Invalid bit range in binary patch relocation"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + relocs.emplace_back(relocType, expression, byteOffset, bitOffset, bitCount); + } + currentGroup->list_patches.emplace_back(new PatchEntryInstruction((sint32)entryIndex + 1, patchAddress, patchData, relocs)); + } + else + { + LogPatchesSyntaxError(-1, "Unknown entry type in binary patch file"); + CancelParsingPatches(); + delete currentGroup; + return false; + } + } + + AddPatchGroup(currentGroup); + } + + if (patchesStream.hasError() || !patchesStream.isEndOfStream()) + { + LogPatchesSyntaxError(-1, "Trailing or malformed data in binary patch file"); + CancelParsingPatches(); + return false; + } + + return true; +} + static inline uint32 INVALID_ORIGIN = 0xFFFFFFFF; bool GraphicPack2::ParseCemuPatchesTxtInternal(MemStreamReader& patchesStream) @@ -378,22 +698,26 @@ bool GraphicPack2::ParseCemuPatchesTxtInternal(MemStreamReader& patchesStream) uint32 overwriteOrigin = INVALID_ORIGIN; if (parser.compareCharacter(0, '0') && parser.compareCharacterI(1, 'x')) { + StringTokenParser addressParserBackup; + parser.storeParserState(&addressParserBackup); uint32 patchedAddress; - if (!parser.parseU32(patchedAddress)) + if (parser.parseU32(patchedAddress)) { - LogPatchesSyntaxError(lineNumber, "Malformed address"); - CancelParsingPatches(); - return false; + if (parser.matchWordI("=")) + { + parser.skipWhitespaces(); + parser.trimWhitespaces(); + overwriteOrigin = patchedAddress; + } + else + { + parser.restoreParserState(&addressParserBackup); + } } - if (parser.matchWordI("=") == false) + else { - LogPatchesSyntaxError(lineNumber, "Expected '=' after address"); - CancelParsingPatches(); - return false; + parser.restoreParserState(&addressParserBackup); } - parser.skipWhitespaces(); - parser.trimWhitespaces(); - overwriteOrigin = patchedAddress; } // check for known directives if (parser.matchWordI(".origin")) @@ -457,6 +781,50 @@ bool GraphicPack2::ParseCemuPatchesTxtInternal(MemStreamReader& patchesStream) } } + std::vector rawPatchData; + std::string rawPatchError; + RAW_HEX_PARSE_RESULT rawPatchResult = _parseRawHexPatchData(std::string_view(parser.getCurrentPtr(), parser.getCurrentLen()), rawPatchData, rawPatchError); + if (rawPatchResult == RAW_HEX_PARSE_RESULT::PARSE_ERROR) + { + LogPatchesSyntaxError(lineNumber, rawPatchError); + CancelParsingPatches(); + return false; + } + if (rawPatchResult == RAW_HEX_PARSE_RESULT::MATCH) + { + if (currentGroup == nullptr) + { + LogPatchesSyntaxError(lineNumber, "Raw binary patch data specified outside of a group"); + CancelParsingPatches(); + return false; + } + uint32 patchAddress; + if (overwriteOrigin != INVALID_ORIGIN) + { + patchAddress = overwriteOrigin; + } + else if (originInfo.isValidOrigin()) + { + patchAddress = originInfo.currentOrigin; + originInfo.incrementOrigin((uint32)rawPatchData.size()); + } + else + { + LogPatchesSyntaxError(lineNumber, "Trying to emit raw binary patch data but no address specified. (Declare .origin or prefix line with
= )"); + CancelParsingPatches(); + return false; + } + for (auto& itr : scheduledLabels) + { + itr->setAssignedVA(patchAddress); + currentGroup->list_patches.emplace_back(itr); + } + scheduledLabels.clear(); + std::vector noRelocs; + currentGroup->list_patches.emplace_back(new PatchEntryInstruction(lineNumber, patchAddress, { rawPatchData.data(), rawPatchData.size() }, noRelocs)); + continue; + } + // next we attempt to parse symbol assignment // symbols can be labels or variables. The type is determined by what comes after the symbol name // = defines a variable