feat: add CPB graphic pack patch loading

This commit is contained in:
fooly 2026-07-14 18:56:06 +09:00
parent 878f0c4b39
commit 83c0f2cab0
3 changed files with 423 additions and 44 deletions

View File

@ -278,6 +278,7 @@ private:
void ParseCemuhookPatchesTxtInternal(MemStreamReader& patchesStream);
bool ParseCemuPatchesTxtInternal(MemStreamReader& patchesStream);
bool ParseCemuBinaryPatchesInternal(MemStreamReader& patchesStream);
void CancelParsingPatches();
void ApplyPatchGroups(std::vector<PatchGroup*>& groups, const RPLModule* rpl);

View File

@ -66,56 +66,66 @@ void PatchErrorHandler::showStageErrorMessageBox()
WindowSystem::ShowErrorDialog(errorMsg, _tr("Graphic pack error"), WindowSystem::ErrorCategory::GRAPHIC_PACKS);
}
// loads Cemu-style patches (patch_<anything>.asm)
// loads Cemu-style patches (patch_<anything>.cpb or patch_<anything>.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<uint8> 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<uint8> 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_<name>.asm), stop here if at least one patch file exists
// 2) Load Cemuhook patches.txt
// 1) Load Cemu binary patches (patch_<name>.cpb), stop here if at least one patch file exists
// 2) Load Cemu-style patches (patch_<name>.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();

View File

@ -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<uint8>& 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<double>);
case PPCASM_RELOC::U16:
return sizeof(betype<uint16>);
case PPCASM_RELOC::U8:
return sizeof(betype<uint8>);
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<uint32>);
default:
return 0;
}
}
static bool _readBinaryPatchString(MemStreamReader& patchesStream, std::string& str)
{
uint16 strLength = patchesStream.readBE<uint16>();
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<PatchEntryInstruction*>(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<uint8> 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<PPCAssemblerReloc> 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<uint32>();
if (patchesStream.hasError() || magic != CEMU_PATCH_BINARY_MAGIC)
{
LogPatchesSyntaxError(-1, "Invalid binary patch file header");
CancelParsingPatches();
return false;
}
uint32 groupCount = patchesStream.readBE<uint32>();
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<uint32>();
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<uint32>());
if (patchesStream.hasError())
{
LogPatchesSyntaxError(-1, "Unexpected end of binary patch file while reading moduleMatches");
CancelParsingPatches();
delete currentGroup;
return false;
}
}
uint32 entryCount = patchesStream.readBE<uint32>();
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<uint8>();
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<uint32>();
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>();
uint32 dataSize = patchesStream.readBE<uint32>();
uint32 relocCount = patchesStream.readBE<uint32>();
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<PPCAssemblerReloc> relocs;
for (uint32 relocIndex = 0; relocIndex < relocCount; relocIndex++)
{
uint8 relocTypeRaw = patchesStream.readBE<uint8>();
uint32 byteOffset = patchesStream.readBE<uint32>();
uint8 bitOffset = patchesStream.readBE<uint8>();
uint8 bitCount = patchesStream.readBE<uint8>();
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<uint8> 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 <address> = )");
CancelParsingPatches();
return false;
}
for (auto& itr : scheduledLabels)
{
itr->setAssignedVA(patchAddress);
currentGroup->list_patches.emplace_back(itr);
}
scheduledLabels.clear();
std::vector<PPCAssemblerReloc> 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
// <symbolName> = <expression> defines a variable