VMManager: Support playing back GS dumps

This commit is contained in:
Connor McLaughlin 2022-03-12 23:20:23 +10:00 committed by refractionpcsx2
parent 4d85d916b7
commit 4331ae1925
13 changed files with 482 additions and 15 deletions

View File

@ -28,6 +28,7 @@
#include "pcsx2/Frontend/InputManager.h"
#include "pcsx2/Frontend/ImGuiManager.h"
#include "pcsx2/GS.h"
#include "pcsx2/GSDumpReplayer.h"
#include "pcsx2/HostDisplay.h"
#include "pcsx2/PAD/Host/PAD.h"
#include "pcsx2/PerformanceMetrics.h"
@ -677,6 +678,9 @@ bool Host::BeginPresentFrame(bool frame_skip)
void Host::EndPresentFrame()
{
if (GSDumpReplayer::IsReplayingDump())
GSDumpReplayer::RenderUI();
ImGuiManager::RenderOSD();
s_host_display->EndPresent();
ImGuiManager::NewFrame();

View File

@ -44,9 +44,15 @@
#include "svnrev.h"
static constexpr char DISC_IMAGE_FILTER[] =
QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.elf *.irx *.m3u);;Single-Track Raw Images (*.bin "
"*.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;CSO Images (*.cso);;"
"ELF Executables (*.elf);;IRX Executables (*.irx);;Playlists (*.m3u)");
QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.elf *.irx *.m3u *.gs *.gs.xz);;"
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
"MAME CHD Images (*.chd);;"
"CSO Images (*.cso);;"
"ELF Executables (*.elf);;"
"IRX Executables (*.irx);;"
"Playlists (*.m3u);;"
"GS Dumps (*.gs *.gs.xz)");
const char* MainWindow::DEFAULT_THEME_NAME = "darkfusion";

View File

@ -983,6 +983,7 @@ if(PCSX2_CORE)
Frontend/InputManager.cpp
Frontend/InputSource.cpp
Frontend/LayeredSettingsInterface.cpp
GSDumpReplayer.cpp
HostSettings.cpp
VMManager.cpp
)
@ -992,6 +993,7 @@ if(PCSX2_CORE)
Frontend/InputManager.h
Frontend/InputSource.h
Frontend/LayeredSettingsInterface.h
GSDumpReplayer.h
HostSettings.h
VMManager.h)
endif()

View File

@ -630,3 +630,17 @@ void ImGuiManager::RenderOSD()
DrawOSDMessages();
}
float ImGuiManager::GetGlobalScale()
{
return s_global_scale;
}
ImFont* ImGuiManager::GetStandardFont()
{
return s_standard_font;
}
ImFont* ImGuiManager::GetFixedFont()
{
return s_fixed_font;
}

View File

@ -15,6 +15,8 @@
#pragma once
struct ImFont;
namespace ImGuiManager
{
/// Initializes ImGui, creates fonts, etc.
@ -34,5 +36,14 @@ namespace ImGuiManager
/// Renders any on-screen display elements.
void RenderOSD();
/// Returns the scale of all on-screen elements.
float GetGlobalScale();
/// Returns the standard font for external drawing.
ImFont* GetStandardFont();
/// Returns the fixed-width font for external drawing.
ImFont* GetFixedFont();
} // namespace ImGuiManager

342
pcsx2/GSDumpReplayer.cpp Normal file
View File

@ -0,0 +1,342 @@
#include "PrecompiledHeader.h"
#include <atomic>
#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "imgui.h"
// Has to come before Gif.h
#include "MemoryTypes.h"
#include "Frontend/ImGuiManager.h"
#include "Frontend/GameList.h"
#include "Gif.h"
#include "Gif_Unit.h"
#include "GSDumpReplayer.h"
#include "GS/GSLzma.h"
#include "GS.h"
#include "Host.h"
#include "R3000A.h"
#include "R5900.h"
#include "VMManager.h"
#include "VUmicro.h"
static void GSDumpReplayerCpuReserve();
static void GSDumpReplayerCpuShutdown();
static void GSDumpReplayerCpuReset();
static void GSDumpReplayerCpuStep();
static void GSDumpReplayerCpuExecute();
static void GSDumpReplayerCpuCheckExecutionState();
static void GSDumpReplayerCpuThrowException(const BaseException& ex);
static void GSDumpReplayerCpuThrowCpuException(const BaseR5900Exception& ex);
static void GSDumpReplayerCpuClear(u32 addr, u32 size);
static uint GSDumpReplayerCpuGetCacheReserve();
static void GSDumpReplayerCpuSetCacheReserve(uint reserveInMegs);
static std::unique_ptr<GSDumpFile> s_dump_file;
static u32 s_current_packet = 0;
static u32 s_dump_frame_number = 0;
static bool s_dump_running = false;
static bool s_needs_state_loaded = false;
static u64 s_frame_ticks = 0;
static u64 s_next_frame_time = 0;
R5900cpu GSDumpReplayerCpu = {
GSDumpReplayerCpuReserve,
GSDumpReplayerCpuShutdown,
GSDumpReplayerCpuReset,
GSDumpReplayerCpuStep,
GSDumpReplayerCpuExecute,
GSDumpReplayerCpuCheckExecutionState,
GSDumpReplayerCpuThrowException,
GSDumpReplayerCpuThrowCpuException,
GSDumpReplayerCpuClear,
GSDumpReplayerCpuGetCacheReserve,
GSDumpReplayerCpuSetCacheReserve};
static InterpVU0 gsDumpVU0;
static InterpVU1 gsDumpVU1;
bool GSDumpReplayer::IsReplayingDump()
{
return static_cast<bool>(s_dump_file);
}
bool GSDumpReplayer::Initialize(const char* filename)
{
Common::Timer timer;
Console.WriteLn("(GSDumpReplayer) Reading file...");
s_dump_file = GSDumpFile::OpenGSDump(filename);
if (!s_dump_file || !s_dump_file->ReadFile())
{
Host::ReportFormattedErrorAsync("GSDumpReplayer", "Failed to open or read '%s'.", filename);
s_dump_file.reset();
return false;
}
Console.WriteLn("(GSDumpReplayer) Read file in %.2f ms.", timer.GetTimeMilliseconds());
// We replace all CPUs.
Cpu = &GSDumpReplayerCpu;
psxCpu = &psxInt;
CpuVU0 = &gsDumpVU0;
CpuVU1 = &gsDumpVU1;
return true;
}
void GSDumpReplayer::Reset()
{
GSDumpReplayerCpuReset();
}
void GSDumpReplayer::Shutdown()
{
Console.WriteLn("(GSDumpReplayer) Shutting down.");
Cpu = nullptr;
psxCpu = nullptr;
CpuVU0 = nullptr;
CpuVU1 = nullptr;
s_dump_file.reset();
}
std::string GSDumpReplayer::GetDumpSerial()
{
std::string ret;
if (!s_dump_file->GetSerial().empty())
{
ret = s_dump_file->GetSerial();
}
else if (s_dump_file->GetCRC() != 0)
{
// old dump files don't have serials, but we have the crc...
// so, let's try searching the game list for a crc match.
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryByCRC(s_dump_file->GetCRC());
if (entry)
ret = entry->serial;
}
return ret;
}
u32 GSDumpReplayer::GetDumpCRC()
{
return s_dump_file->GetCRC();
}
void GSDumpReplayerCpuReserve()
{
}
void GSDumpReplayerCpuShutdown()
{
}
void GSDumpReplayerCpuReset()
{
s_needs_state_loaded = true;
s_current_packet = 0;
s_dump_frame_number = 0;
}
static void GSDumpReplayerLoadInitialState()
{
// reset GS registers to initial dump values
std::memcpy(PS2MEM_GS, s_dump_file->GetRegsData().data(),
std::min(Ps2MemSize::GSregs, static_cast<u32>(s_dump_file->GetRegsData().size())));
// load GS state
freezeData fd = {static_cast<int>(s_dump_file->GetStateData().size()),
const_cast<u8*>(s_dump_file->GetStateData().data())};
MTGS_FreezeData mfd = {&fd, 0};
GetMTGS().Freeze(FreezeAction::Load, mfd);
if (mfd.retval != 0)
Host::ReportFormattedErrorAsync("GSDumpReplayer", "Failed to load GS state.");
}
static void GSDumpReplayerSendPacketToMTGS(GIF_PATH path, const u8* data, u32 length)
{
pxAssert((length % 16) == 0);
Gif_Path& gifPath = gifUnit.gifPath[path];
gifPath.CopyGSPacketData(const_cast<u8*>(data), length);
GS_Packet gsPack;
gsPack.offset = gifPath.curOffset;
gsPack.size = length;
gifPath.curOffset += length;
Gif_AddCompletedGSPacket(gsPack, path);
}
static void GSDumpReplayerUpdateFrameLimit()
{
constexpr u32 default_frame_limit = 60;
const u32 frame_limit = static_cast<u32>(default_frame_limit * EmuConfig.GS.LimitScalar);
if (frame_limit > 0)
s_frame_ticks = (GetTickFrequency() + (frame_limit / 2)) / frame_limit;
else
s_frame_ticks = 0;
}
static void GSDumpReplayerFrameLimit()
{
if (s_frame_ticks == 0)
return;
// Frame limiter
u64 now = GetCPUTicks();
const s64 ms = GetTickFrequency() / 1000;
const s64 sleep = s_next_frame_time - now - ms;
if (sleep > ms)
Threading::Sleep(sleep / ms);
while ((now = GetCPUTicks()) < s_next_frame_time)
ShortSpin();
s_next_frame_time = std::max(now, s_next_frame_time + s_frame_ticks);
}
void GSDumpReplayerCpuStep()
{
if (s_needs_state_loaded)
{
GSDumpReplayerLoadInitialState();
s_needs_state_loaded = false;
}
const GSDumpFile::GSData& packet = s_dump_file->GetPackets()[s_current_packet];
s_current_packet = (s_current_packet + 1) % static_cast<u32>(s_dump_file->GetPackets().size());
if (s_current_packet == 0)
s_dump_frame_number = 0;
switch (packet.id)
{
case GSDumpTypes::GSType::Transfer:
{
switch (packet.path)
{
case GSDumpTypes::GSTransferPath::Path1Old:
{
std::unique_ptr<u8[]> data(new u8[16384]);
const s32 addr = 16384 - packet.length;
std::memcpy(data.get(), packet.data.get() + addr, packet.length);
GSDumpReplayerSendPacketToMTGS(GIF_PATH_1, data.get(), packet.length);
}
break;
case GSDumpTypes::GSTransferPath::Path1New:
case GSDumpTypes::GSTransferPath::Path2:
case GSDumpTypes::GSTransferPath::Path3:
{
GSDumpReplayerSendPacketToMTGS(static_cast<GIF_PATH>(static_cast<u8>(packet.path) - 1),
reinterpret_cast<const u8*>(packet.data.get()), packet.length);
}
break;
default:
break;
}
break;
}
case GSDumpTypes::GSType::VSync:
{
s_dump_frame_number++;
GSDumpReplayerCpuCheckExecutionState();
GSDumpReplayerUpdateFrameLimit();
GSDumpReplayerFrameLimit();
GetMTGS().PostVsyncStart(false);
VMManager::Internal::VSyncOnCPUThread();
}
break;
case GSDumpTypes::GSType::ReadFIFO2:
{
std::unique_ptr<char[]> arr(new char[*((int*)packet.data.get())]);
GSreadFIFO2((u8*)arr.get(), *((int*)packet.data.get()));
}
break;
case GSDumpTypes::GSType::Registers:
{
std::memcpy(PS2MEM_GS, packet.data.get(), std::min<s32>(packet.length, Ps2MemSize::GSregs));
}
break;
}
}
void GSDumpReplayerCpuExecute()
{
s_dump_running = true;
s_next_frame_time = GetCPUTicks();
while (s_dump_running)
{
GSDumpReplayerCpuStep();
}
}
void GSDumpReplayerCpuCheckExecutionState()
{
if (VMManager::Internal::IsExecutionInterrupted())
s_dump_running = false;
}
void GSDumpReplayerCpuThrowException(const BaseException& ex)
{
}
void GSDumpReplayerCpuThrowCpuException(const BaseR5900Exception& ex)
{
}
void GSDumpReplayerCpuClear(u32 addr, u32 size)
{
}
uint GSDumpReplayerCpuGetCacheReserve()
{
return 0;
}
void GSDumpReplayerCpuSetCacheReserve(uint reserveInMegs)
{
}
void GSDumpReplayer::RenderUI()
{
const float scale = ImGuiManager::GetGlobalScale();
const float shadow_offset = std::ceil(1.0f * scale);
const float margin = std::ceil(10.0f * scale);
const float spacing = std::ceil(5.0f * scale);
float position_y = margin;
ImDrawList* dl = ImGui::GetBackgroundDrawList();
ImFont* font = ImGuiManager::GetFixedFont();
FastFormatAscii text;
ImVec2 text_size;
#define DRAW_LINE(font, text, color) \
do \
{ \
text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), -1.0f, (text), nullptr, nullptr); \
dl->AddText(font, font->FontSize, ImVec2(margin + shadow_offset, position_y + shadow_offset), IM_COL32(0, 0, 0, 100), (text)); \
dl->AddText(font, font->FontSize, ImVec2(margin, position_y), color, (text)); \
position_y += text_size.y + spacing; \
} while (0)
text.Write("Dump Frame: %u", s_dump_frame_number);
DRAW_LINE(font, text.c_str(), IM_COL32(255, 255, 255, 255));
text.Clear();
text.Write("Packet Number: %u/%u", s_current_packet, static_cast<u32>(s_dump_file->GetPackets().size()));
DRAW_LINE(font, text.c_str(), IM_COL32(255, 255, 255, 255));
#undef DRAW_LINE
}

15
pcsx2/GSDumpReplayer.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
namespace GSDumpReplayer
{
bool IsReplayingDump();
bool Initialize(const char* filename);
void Reset();
void Shutdown();
std::string GetDumpSerial();
u32 GetDumpCRC();
void RenderUI();
}

View File

@ -49,6 +49,10 @@ BIOS
#include "common/PageFaultSource.h"
#ifdef PCSX2_CORE
#include "GSDumpReplayer.h"
#endif
#ifdef ENABLECACHE
#include "Cache.h"
#endif
@ -850,7 +854,13 @@ void eeMemoryReserve::Reset()
vtlb_VMap(0x00000000,0x00000000,0x20000000);
vtlb_VMapUnmap(0x20000000,0x60000000);
if (!LoadBIOS())
#ifdef PCSX2_CORE
const bool needs_bios = !GSDumpReplayer::IsReplayingDump();
#else
constexpr bool needs_bios = true;
#endif
if (needs_bios && !LoadBIOS())
pxFailRel("Failed to load BIOS");
}

View File

@ -27,6 +27,11 @@
#include "common/MemsetFast.inl"
#include "common/Perf.h"
#ifdef PCSX2_CORE
#include "GSDumpReplayer.h"
extern R5900cpu GSDumpReplayerCpu;
#endif
// --------------------------------------------------------------------------------------
// RecompiledCodeReserve (implementations)
@ -576,6 +581,11 @@ void SysCpuProviderPack::ApplyConfig() const
if( EmuConfig.Cpu.Recompiler.EnableVU1 )
CpuVU1 = (BaseVUmicroCPU*)CpuProviders->microVU1;
#ifdef PCSX2_CORE
if (GSDumpReplayer::IsReplayingDump())
Cpu = &GSDumpReplayerCpu;
#endif
}
// Resets all PS2 cpu execution caches, which does not affect that actual PS2 state/condition.

View File

@ -35,6 +35,7 @@
#include "Elfheader.h"
#include "FW.h"
#include "GS.h"
#include "GSDumpReplayer.h"
#include "HostDisplay.h"
#include "HostSettings.h"
#include "IopBios.h"
@ -222,6 +223,10 @@ void VMManager::LoadSettings()
EmuConfig.GS.MaskUserHacks();
EmuConfig.GS.MaskUpscalingHacks();
// Force MTVU off when playing back GS dumps, it doesn't get used.
if (GSDumpReplayer::IsReplayingDump())
EmuConfig.Speedhacks.vuThread = false;
if (HasValidVM())
ApplyGameFixes();
}
@ -454,11 +459,20 @@ void VMManager::UpdateRunningGame(bool force)
// we have the CRC but we're still at the bios and the settings are changed
// (e.g. the user presses TAB to speed up emulation), we don't want to apply the
// settings as if the game is already running (title, loadeding patches, etc).
bool ingame = (ElfCRC && (g_GameLoading || g_GameStarted));
const u32 new_crc = ingame ? ElfCRC : 0;
const std::string crc_string(StringUtil::StdStringFromFormat("%08X", new_crc));
u32 new_crc;
std::string new_serial;
if (!GSDumpReplayer::IsReplayingDump())
{
const bool ingame = (ElfCRC && (g_GameLoading || g_GameStarted));
new_crc = ingame ? ElfCRC : 0;
new_serial = ingame ? SysGetDiscID().ToStdString() : SysGetBiosDiscID().ToStdString();
}
else
{
new_crc = GSDumpReplayer::GetDumpCRC();
new_serial = GSDumpReplayer::GetDumpSerial();
}
std::string new_serial(ingame ? SysGetDiscID().ToStdString() : SysGetBiosDiscID().ToStdString());
if (!force && s_game_crc == new_crc && s_game_serial == new_serial)
return;
@ -488,7 +502,7 @@ void VMManager::UpdateRunningGame(bool force)
ApplySettings();
ForgetLoadedPatches();
LoadPatches(crc_string, true, false);
LoadPatches(StringUtil::StdStringFromFormat("%08X", new_crc), true, false);
GetMTGS().SendGameCRC(new_crc);
Host::OnGameChanged(s_disc_path, s_game_serial, s_game_name, s_game_crc);
@ -544,7 +558,18 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
};
LoadSettings();
ApplyBootParameters(boot_params);
if (IsGSDumpFileName(boot_params.source))
{
CDVDsys_ChangeSource(CDVD_SourceType::NoDisc);
if (!GSDumpReplayer::Initialize(boot_params.source.c_str()))
return false;
}
else
{
ApplyBootParameters(boot_params);
}
EmuConfig.LimiterMode = GetInitialLimiterMode();
Console.WriteLn("Allocating memory map...");
@ -661,17 +686,18 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
frameLimitReset();
cpuReset();
Console.WriteLn("VM subsystems initialized in %.2f ms", init_timer.GetTimeMilliseconds());
s_state.store(VMState::Paused);
Host::OnVMStarted();
UpdateRunningGame(true);
SetEmuThreadAffinities(true);
PerformanceMetrics::Clear();
Console.WriteLn("VM subsystems initialized in %.2f ms", init_timer.GetTimeMilliseconds());
s_state.store(VMState::Paused);
Host::OnVMStarted();
// do we want to load state?
if (!boot_params.save_state.empty())
if (!GSDumpReplayer::IsReplayingDump() && !boot_params.save_state.empty())
{
if (!DoLoadState(boot_params.save_state.c_str()))
{
@ -692,12 +718,16 @@ void VMManager::Shutdown(bool allow_save_resume_state /* = true */)
vu1Thread.WaitVU();
GetMTGS().WaitGS();
if (allow_save_resume_state && ShouldSaveResumeState())
if (!GSDumpReplayer::IsReplayingDump() && allow_save_resume_state && ShouldSaveResumeState())
{
std::string resume_file_name(GetCurrentSaveStateFileName(-1));
if (!resume_file_name.empty() && !DoSaveState(resume_file_name.c_str(), -1))
Console.Error("Failed to save resume state");
}
else if (GSDumpReplayer::IsReplayingDump())
{
GSDumpReplayer::Shutdown();
}
{
std::unique_lock lock(s_info_mutex);
@ -790,6 +820,9 @@ std::string VMManager::GetCurrentSaveStateFileName(s32 slot)
bool VMManager::DoLoadState(const char* filename)
{
if (GSDumpReplayer::IsReplayingDump())
return false;
try
{
Host::OnSaveStateLoading(filename);
@ -808,6 +841,9 @@ bool VMManager::DoLoadState(const char* filename)
bool VMManager::DoSaveState(const char* filename, s32 slot_for_message)
{
if (GSDumpReplayer::IsReplayingDump())
return false;
try
{
std::unique_ptr<ArchiveEntryList> elist = std::make_unique<ArchiveEntryList>(new VmStateBuffer(L"Zippable Savestate"));
@ -920,6 +956,11 @@ bool VMManager::IsElfFileName(const std::string& path)
return (StringUtil::Strcasecmp(&path[pos], ".elf") == 0);
}
bool VMManager::IsGSDumpFileName(const std::string& path)
{
return (StringUtil::EndsWithNoCase(path, ".gs") || StringUtil::EndsWithNoCase(path, ".gs.xz"));
}
void VMManager::SetBootParametersForPath(const std::string& path, VMBootParameters* params)
{
if (IsElfFileName(path))
@ -927,6 +968,11 @@ void VMManager::SetBootParametersForPath(const std::string& path, VMBootParamete
params->elf_override = path;
params->source_type = CDVD_SourceType::NoDisc;
}
else if (IsGSDumpFileName(path))
{
params->source_type = CDVD_SourceType::NoDisc;
params->source = path;
}
else if (!path.empty())
{
params->source_type = CDVD_SourceType::Iso;

View File

@ -135,6 +135,9 @@ namespace VMManager
/// Returns true if the specified path is an ELF.
bool IsElfFileName(const std::string& path);
/// Returns true if the specified path is a GS Dump.
bool IsGSDumpFileName(const std::string& path);
/// Updates boot parameters for a given start filename. If it's an elf, it'll set elf_override, otherwise source.
void SetBootParametersForPath(const std::string& path, VMBootParameters* params);

View File

@ -186,6 +186,7 @@
<ClCompile Include="GameDatabase.cpp" />
<ClCompile Include="Gif_Logger.cpp" />
<ClCompile Include="Gif_Unit.cpp" />
<ClCompile Include="GSDumpReplayer.cpp" />
<ClCompile Include="GS\Renderers\DX11\D3D.cpp" />
<ClCompile Include="GS\Renderers\HW\GSTextureReplacementLoaders.cpp" />
<ClCompile Include="GS\Renderers\HW\GSTextureReplacements.cpp" />
@ -488,6 +489,7 @@
<ClInclude Include="Frontend\XInputSource.h" />
<ClInclude Include="GameDatabase.h" />
<ClInclude Include="Gif_Unit.h" />
<ClInclude Include="GSDumpReplayer.h" />
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h" />
<ClInclude Include="GS\Window\GSwxDialog.h" />

View File

@ -1202,6 +1202,7 @@
<ClCompile Include="x86\iR5900Analysis.cpp">
<Filter>System\Ps2\EmotionEngine\EE\Dynarec</Filter>
</ClCompile>
<ClCompile Include="GSDumpReplayer.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Patch.h">
@ -1985,6 +1986,7 @@
<ClInclude Include="x86\iR5900Analysis.h">
<Filter>System\Ps2\EmotionEngine\EE\Dynarec</Filter>
</ClInclude>
<ClInclude Include="GSDumpReplayer.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="GS\GS.rc">