#include "Cafe/GraphicPack/GraphicPack2.h" #include "Common/FileStream.h" #include "util/helpers/StringParser.h" #include "Cemu/PPCAssembler/ppcAssembler.h" #include "Cafe/OS/RPL/rpl_structs.h" sint32 GraphicPack2::GetLengthWithoutComment(const char* str, size_t length) { sint32 index = 0; bool isInString = false; while (index < length) { const char c = str[index]; if (c == '\"') isInString = !isInString; else if (c == '#' || c == ';') { if (!isInString) return index; } index++; } return (sint32)length; } void GraphicPack2::LogPatchesSyntaxError(sint32 lineNumber, std::string_view errorMsg) { cemuLog_log(LogType::Force, fmt::format(L"Syntax error while parsing patch for graphic pack '{}':", this->GetFilename())); if(lineNumber >= 0) cemuLog_log(LogType::Force, fmt::format("Line {0}: {1}", lineNumber, errorMsg)); else cemuLog_log(LogType::Force, fmt::format("{0}", errorMsg)); list_patchGroups.clear(); } void GraphicPack2::CancelParsingPatches() { // unload everything, set error flag cemu_assert_debug(false); } void GraphicPack2::AddPatchGroup(PatchGroup* group) { if (group->list_moduleMatches.empty()) { LogPatchesSyntaxError(-1, fmt::format("Group \"{}\" has no moduleMatches definition", group->name)); CancelParsingPatches(); delete group; return; } // calculate code cave size uint32 codeCaveMaxAddr = 0; for (auto& itr : group->list_patches) { PatchEntryInstruction* patchData = dynamic_cast(itr); if (patchData) { uint32 patchAddr = patchData->getAddr(); if (patchAddr < 0x00100000) { // everything in low 1MB of memory we consider part of the code cave codeCaveMaxAddr = std::max(codeCaveMaxAddr, patchAddr + patchData->getSize()); } } } uint32 numEstimatedCodeCaveInstr = codeCaveMaxAddr / 4; if (group->list_patches.size() < (numEstimatedCodeCaveInstr / 8)) { // if less than 1/8th of the code cave is filled print a warning forceLog_printf("Graphic pack patches: Code cave for group [%s] in gfx pack \"%s\" ranges from 0 to 0x%x but has only few instructions. Is this intentional?", group->name.c_str(), this->m_name.c_str(), codeCaveMaxAddr); } group->codeCaveSize = codeCaveMaxAddr; list_patchGroups.emplace_back(group); } void GraphicPack2::ParseCemuhookPatchesTxtInternal(MemStreamReader& patchesStream) { sint32 lineNumber = 0; PatchGroup* currentGroup = nullptr; while (true) { auto lineStr = patchesStream.readLine(); lineNumber++; if (patchesStream.hasError()) break; // trim comment size_t lineLength = GetLengthWithoutComment(lineStr.data(), lineStr.size()); StringTokenParser parser(lineStr.data(), (sint32)lineLength); // skip whitespaces at the beginning parser.skipWhitespaces(); // parse line if (parser.isEndOfString()) continue; if (parser.compareCharacter(0, '[')) { // group parser.skipCharacters(1); // find end of group name const char* groupNameStr = parser.getCurrentPtr(); sint32 groupNameLength = parser.skipToCharacter(']'); if (groupNameLength < 0) { LogPatchesSyntaxError(lineNumber, "Expected ']'"); CancelParsingPatches(); return; } parser.skipCharacters(1); // skip the ']' parser.skipWhitespaces(); if (!parser.isEndOfString()) { LogPatchesSyntaxError(lineNumber, "Unexpected characters after ']'"); CancelParsingPatches(); return; } // begin new group if (currentGroup) { AddPatchGroup(currentGroup); } currentGroup = new PatchGroup(this, groupNameStr, groupNameLength); } else if (parser.compareCharacter(0, '0') && parser.compareCharacterI(1, 'x')) { // if the line starts with a hex address then it is a patched location uint32 patchedAddress; if (!parser.parseU32(patchedAddress)) { LogPatchesSyntaxError(lineNumber, "Malformed address"); CancelParsingPatches(); return; } if (parser.matchWordI("=") == false) { LogPatchesSyntaxError(lineNumber, "Expected '=' after address"); CancelParsingPatches(); return; } parser.skipWhitespaces(); parser.trimWhitespaces(); // assemble instruction std::string instrText(parser.getCurrentPtr(), parser.getCurrentLen()); PPCAssemblerInOut ctx{}; ctx.virtualAddress = patchedAddress; if (!ppcAssembler_assembleSingleInstruction(instrText.c_str(), &ctx)) { LogPatchesSyntaxError(lineNumber, fmt::format("Error in assembler: {}", ctx.errorMsg)); CancelParsingPatches(); return; } currentGroup->list_patches.emplace_back(new PatchEntryInstruction(lineNumber, patchedAddress, { ctx.outputData.data(), ctx.outputData.size() }, ctx.list_relocs)); } else if (parser.matchWordI("moduleMatches")) { if (currentGroup == nullptr) { LogPatchesSyntaxError(lineNumber, "Specified 'ModuleMatches' outside of a group"); CancelParsingPatches(); return; } if (parser.matchWordI("=") == false) { LogPatchesSyntaxError(lineNumber, "Expected '=' after ModuleMatches"); CancelParsingPatches(); return; } // read the checksums while (true) { uint32 checksum = 0; if (parser.parseU32(checksum) == false) { LogPatchesSyntaxError(lineNumber, "Invalid value for ModuleMatches"); CancelParsingPatches(); return; } currentGroup->list_moduleMatches.emplace_back(checksum); if (parser.matchWordI(",") == false) break; } parser.skipWhitespaces(); if (!parser.isEndOfString()) { LogPatchesSyntaxError(lineNumber, "Unexpected character in line"); CancelParsingPatches(); return; } continue; } else { // Cemuhook requires that user defined symbols start with _ but we are more lenient and allow them to start with letters too // the downside is that there is some ambiguity and parsing gets a little bit more complex // check for = pattern StringTokenParser bakParser; const char* symbolStr; sint32 symbolLen; parser.storeParserState(&bakParser); if (parser.parseSymbolName(symbolStr, symbolLen) && parser.matchWordI("=")) { // matches pattern: = ... parser.skipWhitespaces(); parser.trimWhitespaces(); const char* expressionStr = parser.getCurrentPtr(); sint32 expressionLen = parser.getCurrentLen(); // create entry for symbol value assignment currentGroup->list_patches.emplace_back(new PatchEntryCemuhookSymbolValue(lineNumber, symbolStr, symbolLen, expressionStr, expressionLen)); continue; } else { LogPatchesSyntaxError(lineNumber, fmt::format("Invalid syntax")); CancelParsingPatches(); return; } } } if (currentGroup) AddPatchGroup(currentGroup); } static inline uint32 INVALID_ORIGIN = 0xFFFFFFFF; bool GraphicPack2::ParseCemuPatchesTxtInternal(MemStreamReader& patchesStream) { sint32 lineNumber = 0; PatchGroup* currentGroup = nullptr; struct { void reset() { currentOrigin = INVALID_ORIGIN; codeCaveOrigin = 0; } void setOrigin(uint32 origin) { currentOrigin = origin; } void setOriginCodeCave() { currentOrigin = codeCaveOrigin; } bool isValidOrigin() { return currentOrigin != INVALID_ORIGIN; } void incrementOrigin(uint32 size) { currentOrigin += size; if (currentOrigin <= 32 * 1024 * 1024) codeCaveOrigin = std::max(codeCaveOrigin, currentOrigin); } uint32 currentOrigin{}; uint32 codeCaveOrigin{}; }originInfo; // labels dont get emitted immediately, instead they are assigned a VA after the next alignment zone std::vector scheduledLabels; // this is to prevent code like this from putting alignment bytes after the label. (The label 'sticks' to the data after it) // .byte 123 // Label: // BLR auto flushLabels = [&]() { // flush remaining labels for (auto& itr : scheduledLabels) { itr->setAssignedVA(originInfo.currentOrigin); currentGroup->list_patches.emplace_back(itr); } scheduledLabels.clear(); }; while (true) { size_t lineLength; auto lineStr = patchesStream.readLine(); lineNumber++; if (patchesStream.hasError()) break; // trim comment lineLength = GetLengthWithoutComment(lineStr.data(), lineStr.size()); StringTokenParser parser(lineStr.data(), (sint32)lineLength); // skip whitespaces at the beginning parser.skipWhitespaces(); // parse line if (parser.isEndOfString()) continue; if (parser.compareCharacter(0, '[')) { // group parser.skipCharacters(1); // find end of group name const char* groupNameStr = parser.getCurrentPtr(); sint32 groupNameLength = parser.skipToCharacter(']'); if (groupNameLength < 0) { LogPatchesSyntaxError(lineNumber, "Expected ']'"); CancelParsingPatches(); return false; } parser.skipCharacters(1); // skip the ']' parser.skipWhitespaces(); if (!parser.isEndOfString()) { LogPatchesSyntaxError(lineNumber, "Unexpected characters after ']'"); CancelParsingPatches(); return false; } // begin new group if (currentGroup) { flushLabels(); AddPatchGroup(currentGroup); } currentGroup = new PatchGroup(this, groupNameStr, groupNameLength); // reset origin tracking originInfo.reset(); continue; } else if (parser.matchWordI("moduleMatches")) { if (currentGroup == nullptr) { LogPatchesSyntaxError(lineNumber, "Specified 'ModuleMatches' outside of a group"); CancelParsingPatches(); return false; } if (parser.matchWordI("=") == false) { LogPatchesSyntaxError(lineNumber, "Expected '=' after ModuleMatches"); CancelParsingPatches(); return false; } // read the checksums while (true) { uint32 checksum = 0; if (parser.parseU32(checksum) == false) { LogPatchesSyntaxError(lineNumber, "Invalid value for ModuleMatches"); CancelParsingPatches(); return false; } currentGroup->list_moduleMatches.emplace_back(checksum); if (parser.matchWordI(",") == false) break; } parser.skipWhitespaces(); if (!parser.isEndOfString()) { LogPatchesSyntaxError(lineNumber, "Unexpected character"); CancelParsingPatches(); return false; } continue; } // if a line starts with = then it temporarily overwrites the origin for the current line uint32 overwriteOrigin = INVALID_ORIGIN; if (parser.compareCharacter(0, '0') && parser.compareCharacterI(1, 'x')) { uint32 patchedAddress; if (!parser.parseU32(patchedAddress)) { LogPatchesSyntaxError(lineNumber, "Malformed address"); CancelParsingPatches(); return false; } if (parser.matchWordI("=") == false) { LogPatchesSyntaxError(lineNumber, "Expected '=' after address"); CancelParsingPatches(); return false; } parser.skipWhitespaces(); parser.trimWhitespaces(); overwriteOrigin = patchedAddress; } // check for known directives if (parser.matchWordI(".origin")) { // .origin = directive if (overwriteOrigin != INVALID_ORIGIN) { LogPatchesSyntaxError(lineNumber, fmt::format(".origin directive must appear alone without
= prefix.")); CancelParsingPatches(); return false; } if (!parser.matchWordI("=")) { LogPatchesSyntaxError(lineNumber, fmt::format("Missing '=' after .origin")); CancelParsingPatches(); return false; } // parse origin uint32 originAddress; if (parser.matchWordI("codecave")) { // keyword codecave means we set the origin to the end of the current known codecave size originInfo.setOriginCodeCave(); } else if(parser.parseU32(originAddress)) { // hex address originInfo.setOrigin(originAddress); } else { LogPatchesSyntaxError(lineNumber, fmt::format("\'.origin =\' must be followed by the keyword codecave or a valid address")); CancelParsingPatches(); return false; } 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 // : defines a label StringTokenParser bakParser; const char* symbolStr; sint32 symbolLen; parser.storeParserState(&bakParser); // check for pattern : if (parser.parseSymbolName(symbolStr, symbolLen) && parser.matchWordI(":")) { // label parser.skipWhitespaces(); if (!parser.isEndOfString()) { LogPatchesSyntaxError(lineNumber, fmt::format("Unexpected characters after label")); CancelParsingPatches(); return false; } uint32 labelAddress; if (overwriteOrigin != INVALID_ORIGIN) labelAddress = overwriteOrigin; else { if (!originInfo.isValidOrigin()) { LogPatchesSyntaxError(lineNumber, fmt::format("Defined label has no address assigned or there is no active .origin")); CancelParsingPatches(); return false; } labelAddress = originInfo.currentOrigin; } if (overwriteOrigin == INVALID_ORIGIN) { // if label is part of code flow, delay emitting it until the next data instruction // this is so we can avoid generating alignment padding, whose size is unknown in advance, between labels and data instructions scheduledLabels.emplace_back(new PatchEntryLabel(lineNumber, symbolStr, symbolLen)); } else { PatchEntryLabel* patchLabel = new PatchEntryLabel(lineNumber, symbolStr, symbolLen); patchLabel->setAssignedVA(labelAddress); currentGroup->list_patches.emplace_back(patchLabel); } continue; } parser.restoreParserState(&bakParser); // check for pattern = if (parser.parseSymbolName(symbolStr, symbolLen) && parser.matchWordI("=")) { // variable definition parser.skipWhitespaces(); parser.trimWhitespaces(); const char* expressionStr = parser.getCurrentPtr(); sint32 expressionLen = parser.getCurrentLen(); // create entry for symbol/variable value assignment currentGroup->list_patches.emplace_back(new PatchEntryVariableValue(lineNumber, symbolStr, symbolLen, PATCHVARTYPE::UINT, expressionStr, expressionLen)); continue; } // if all patterns mismatch then we assume it's an assembly instruction parser.restoreParserState(&bakParser); std::string instrText(parser.getCurrentPtr(), parser.getCurrentLen()); PPCAssemblerInOut ctx{}; ctx.forceNoAlignment = overwriteOrigin != INVALID_ORIGIN; // dont auto-align when a fixed address is assigned if (overwriteOrigin != INVALID_ORIGIN) ctx.virtualAddress = overwriteOrigin; else if(originInfo.isValidOrigin()) ctx.virtualAddress = originInfo.currentOrigin; else { LogPatchesSyntaxError(lineNumber, fmt::format("Trying to assemble line but no address specified. (Declare .origin or prefix line with
= )")); CancelParsingPatches(); return false; } if (!ppcAssembler_assembleSingleInstruction(instrText.c_str(), &ctx)) { LogPatchesSyntaxError(lineNumber, fmt::format("Error in assembler: {}", ctx.errorMsg)); CancelParsingPatches(); return false; } cemu_assert_debug(ctx.alignmentRequirement != 0); if (overwriteOrigin == INVALID_ORIGIN) { originInfo.incrementOrigin((sint32)ctx.alignmentPaddingSize); // alignment padding originInfo.incrementOrigin((sint32)ctx.outputData.size()); // instruction size } // flush labels for (auto& itr : scheduledLabels) { itr->setAssignedVA(ctx.virtualAddressAligned); currentGroup->list_patches.emplace_back(itr); } scheduledLabels.clear(); // append instruction currentGroup->list_patches.emplace_back(new PatchEntryInstruction(lineNumber, ctx.virtualAddressAligned, { ctx.outputData.data(), ctx.outputData.size() }, ctx.list_relocs)); } flushLabels(); if (currentGroup) AddPatchGroup(currentGroup); return true; }