mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-11-15 05:47:56 -07:00
f3b8a985e7
These games are erroneously zeroing buffers before they can be fully copied to ARAM by DMA. The responsible memset() calls are followed by a call to DVDRead() which issues dcbi instructions that effectively cancel the memset() on real hardware. Because Dolphin lacks dcache emulation, the effects of the memset() calls are observed, which causes missing audio. In a comment on the original bug, phire noted that the issue can be corrected by simply nop'ing out the offending memset() calls. Because the games dynamically load different .rel executables based on the character and/or language, the addresses of these calls can vary. To deal generally with the problem of code being dynamically loaded to fixed, known addresses, the patch engine is extended to support conditional patches which require a match against a known value. This sort of thing is already achievable with Action Replay/Gecko codes, but their use depends on enabling cheats globally in Dolphin, which is not a prerequisite shared by patches. Patches are included for every region, character, and language combination. They are enabled by default. The end result is an approximation of the games' behavior on real hardware without the associated complexity of proper dcache emulation. https://bugs.dolphin-emu.org/issues/9840
279 lines
7.2 KiB
C++
279 lines
7.2 KiB
C++
// Copyright 2008 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
// PatchEngine
|
|
// Supports simple memory patches, and has a partial Action Replay implementation
|
|
// in ActionReplay.cpp/h.
|
|
|
|
#include "Core/PatchEngine.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <iterator>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Common/Assert.h"
|
|
#include "Common/IniFile.h"
|
|
#include "Common/StringUtil.h"
|
|
|
|
#include "Core/ActionReplay.h"
|
|
#include "Core/CheatCodes.h"
|
|
#include "Core/Config/MainSettings.h"
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/GeckoCode.h"
|
|
#include "Core/GeckoCodeConfig.h"
|
|
#include "Core/PowerPC/MMU.h"
|
|
#include "Core/PowerPC/PowerPC.h"
|
|
|
|
namespace PatchEngine
|
|
{
|
|
constexpr std::array<const char*, 3> s_patch_type_strings{{
|
|
"byte",
|
|
"word",
|
|
"dword",
|
|
}};
|
|
|
|
static std::vector<Patch> s_on_frame;
|
|
static std::map<u32, int> s_speed_hacks;
|
|
|
|
const char* PatchTypeAsString(PatchType type)
|
|
{
|
|
return s_patch_type_strings.at(static_cast<int>(type));
|
|
}
|
|
|
|
void LoadPatchSection(const std::string& section, std::vector<Patch>& patches, IniFile& globalIni,
|
|
IniFile& localIni)
|
|
{
|
|
const IniFile* inis[2] = {&globalIni, &localIni};
|
|
|
|
for (const IniFile* ini : inis)
|
|
{
|
|
std::vector<std::string> lines;
|
|
Patch currentPatch;
|
|
ini->GetLines(section, &lines);
|
|
|
|
for (std::string& line : lines)
|
|
{
|
|
if (line.empty())
|
|
continue;
|
|
|
|
if (line[0] == '$')
|
|
{
|
|
// Take care of the previous code
|
|
if (!currentPatch.name.empty())
|
|
{
|
|
patches.push_back(currentPatch);
|
|
}
|
|
currentPatch.entries.clear();
|
|
|
|
// Set name and whether the patch is user defined
|
|
currentPatch.name = line.substr(1, line.size() - 1);
|
|
currentPatch.user_defined = (ini == &localIni);
|
|
}
|
|
else
|
|
{
|
|
std::string::size_type loc = line.find('=');
|
|
|
|
if (loc != std::string::npos)
|
|
{
|
|
line[loc] = ':';
|
|
}
|
|
|
|
const std::vector<std::string> items = SplitString(line, ':');
|
|
|
|
if (items.size() >= 3)
|
|
{
|
|
PatchEntry pE;
|
|
bool success = true;
|
|
success &= TryParse(items[0], &pE.address);
|
|
success &= TryParse(items[2], &pE.value);
|
|
if (items.size() >= 4)
|
|
{
|
|
success &= TryParse(items[3], &pE.comparand);
|
|
pE.conditional = true;
|
|
}
|
|
|
|
const auto iter =
|
|
std::find(s_patch_type_strings.begin(), s_patch_type_strings.end(), items[1]);
|
|
pE.type = PatchType(std::distance(s_patch_type_strings.begin(), iter));
|
|
|
|
success &= (pE.type != (PatchType)3);
|
|
if (success)
|
|
{
|
|
currentPatch.entries.push_back(pE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!currentPatch.name.empty() && !currentPatch.entries.empty())
|
|
{
|
|
patches.push_back(currentPatch);
|
|
}
|
|
|
|
ReadEnabledAndDisabled(*ini, section, &patches);
|
|
|
|
if (ini == &globalIni)
|
|
{
|
|
for (Patch& patch : patches)
|
|
patch.default_enabled = patch.enabled;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void LoadSpeedhacks(const std::string& section, IniFile& ini)
|
|
{
|
|
std::vector<std::string> keys;
|
|
ini.GetKeys(section, &keys);
|
|
for (const std::string& key : keys)
|
|
{
|
|
std::string value;
|
|
ini.GetOrCreateSection(section)->Get(key, &value, "BOGUS");
|
|
if (value != "BOGUS")
|
|
{
|
|
u32 address;
|
|
u32 cycles;
|
|
bool success = true;
|
|
success &= TryParse(key, &address);
|
|
success &= TryParse(value, &cycles);
|
|
if (success)
|
|
{
|
|
s_speed_hacks[address] = static_cast<int>(cycles);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int GetSpeedhackCycles(const u32 addr)
|
|
{
|
|
const auto iter = s_speed_hacks.find(addr);
|
|
if (iter == s_speed_hacks.end())
|
|
return 0;
|
|
|
|
return iter->second;
|
|
}
|
|
|
|
void LoadPatches()
|
|
{
|
|
IniFile merged = SConfig::GetInstance().LoadGameIni();
|
|
IniFile globalIni = SConfig::GetInstance().LoadDefaultGameIni();
|
|
IniFile localIni = SConfig::GetInstance().LoadLocalGameIni();
|
|
|
|
LoadPatchSection("OnFrame", s_on_frame, globalIni, localIni);
|
|
|
|
// Check if I'm syncing Codes
|
|
if (Config::Get(Config::MAIN_CODE_SYNC_OVERRIDE))
|
|
{
|
|
Gecko::SetSyncedCodesAsActive();
|
|
ActionReplay::SetSyncedCodesAsActive();
|
|
}
|
|
else
|
|
{
|
|
Gecko::SetActiveCodes(Gecko::LoadCodes(globalIni, localIni));
|
|
ActionReplay::LoadAndApplyCodes(globalIni, localIni);
|
|
}
|
|
|
|
LoadSpeedhacks("Speedhacks", merged);
|
|
}
|
|
|
|
static void ApplyPatches(const std::vector<Patch>& patches)
|
|
{
|
|
for (const Patch& patch : patches)
|
|
{
|
|
if (patch.enabled)
|
|
{
|
|
for (const PatchEntry& entry : patch.entries)
|
|
{
|
|
u32 addr = entry.address;
|
|
u32 value = entry.value;
|
|
u32 comparand = entry.comparand;
|
|
switch (entry.type)
|
|
{
|
|
case PatchType::Patch8Bit:
|
|
if (!entry.conditional || PowerPC::HostRead_U8(addr) == static_cast<u8>(comparand))
|
|
PowerPC::HostWrite_U8(static_cast<u8>(value), addr);
|
|
break;
|
|
case PatchType::Patch16Bit:
|
|
if (!entry.conditional || PowerPC::HostRead_U16(addr) == static_cast<u16>(comparand))
|
|
PowerPC::HostWrite_U16(static_cast<u16>(value), addr);
|
|
break;
|
|
case PatchType::Patch32Bit:
|
|
if (!entry.conditional || PowerPC::HostRead_U32(addr) == comparand)
|
|
PowerPC::HostWrite_U32(value, addr);
|
|
break;
|
|
default:
|
|
// unknown patchtype
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Requires MSR.DR, MSR.IR
|
|
// There's no perfect way to do this, it's just a heuristic.
|
|
// We require at least 2 stack frames, if the stack is shallower than that then it won't work.
|
|
static bool IsStackSane()
|
|
{
|
|
DEBUG_ASSERT(MSR.DR && MSR.IR);
|
|
|
|
// Check the stack pointer
|
|
u32 SP = GPR(1);
|
|
if (!PowerPC::HostIsRAMAddress(SP))
|
|
return false;
|
|
|
|
// Read the frame pointer from the stack (find 2nd frame from top), assert that it makes sense
|
|
u32 next_SP = PowerPC::HostRead_U32(SP);
|
|
if (next_SP <= SP || !PowerPC::HostIsRAMAddress(next_SP) ||
|
|
!PowerPC::HostIsRAMAddress(next_SP + 4))
|
|
return false;
|
|
|
|
// Check the link register makes sense (that it points to a valid IBAT address)
|
|
const u32 address = PowerPC::HostRead_U32(next_SP + 4);
|
|
return PowerPC::HostIsInstructionRAMAddress(address) &&
|
|
0 != PowerPC::HostRead_Instruction(address);
|
|
}
|
|
|
|
bool ApplyFramePatches()
|
|
{
|
|
// Because we're using the VI Interrupt to time this instead of patching the game with a
|
|
// callback hook we can end up catching the game in an exception vector.
|
|
// We deal with this by returning false so that SystemTimers will reschedule us in a few cycles
|
|
// where we can try again after the CPU hopefully returns back to the normal instruction flow.
|
|
if (!MSR.DR || !MSR.IR || !IsStackSane())
|
|
{
|
|
DEBUG_LOG_FMT(ACTIONREPLAY,
|
|
"Need to retry later. CPU configuration is currently incorrect. PC = {:#010x}, "
|
|
"MSR = {:#010x}",
|
|
PC, MSR.Hex);
|
|
return false;
|
|
}
|
|
|
|
ApplyPatches(s_on_frame);
|
|
|
|
// Run the Gecko code handler
|
|
Gecko::RunCodeHandler();
|
|
ActionReplay::RunAllActive();
|
|
|
|
return true;
|
|
}
|
|
|
|
void Shutdown()
|
|
{
|
|
s_on_frame.clear();
|
|
s_speed_hacks.clear();
|
|
ActionReplay::ApplyCodes({});
|
|
Gecko::Shutdown();
|
|
}
|
|
|
|
void Reload()
|
|
{
|
|
Shutdown();
|
|
LoadPatches();
|
|
}
|
|
|
|
} // namespace PatchEngine
|