mirror of
https://github.com/cemu-project/Cemu.git
synced 2024-11-29 20:44:18 +01:00
533 lines
16 KiB
C++
533 lines
16 KiB
C++
#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<PatchEntryInstruction*>(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 <symbolName> = pattern
|
|
StringTokenParser bakParser;
|
|
const char* symbolStr;
|
|
sint32 symbolLen;
|
|
parser.storeParserState(&bakParser);
|
|
if (parser.parseSymbolName(symbolStr, symbolLen) && parser.matchWordI("="))
|
|
{
|
|
// matches pattern: <symbolName> = ...
|
|
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<PatchEntryLabel*> 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 <hex_address> = 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 = <origin> directive
|
|
if (overwriteOrigin != INVALID_ORIGIN)
|
|
{
|
|
LogPatchesSyntaxError(lineNumber, fmt::format(".origin directive must appear alone without <address> = 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
|
|
// <symbolName> = <expression> defines a variable
|
|
// <symbolName>: defines a label
|
|
|
|
StringTokenParser bakParser;
|
|
const char* symbolStr;
|
|
sint32 symbolLen;
|
|
parser.storeParserState(&bakParser);
|
|
|
|
// check for pattern <symbolName>:
|
|
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 <symbolName> =
|
|
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 <address> = )"));
|
|
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;
|
|
}
|