pcsx2/pcsx2/GSDumpReplayer.cpp

403 lines
9.9 KiB
C++

/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2023 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "GS.h"
#include "GS/GSLzma.h"
#include "GSDumpReplayer.h"
#include "GameList.h"
#include "Gif.h"
#include "Gif_Unit.h"
#include "Host.h"
#include "ImGui/ImGuiManager.h"
#include "R3000A.h"
#include "R5900.h"
#include "VMManager.h"
#include "VUmicro.h"
#include "imgui.h"
#include "fmt/core.h"
#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "common/Threading.h"
#include "common/Timer.h"
#include <atomic>
static void GSDumpReplayerCpuReserve();
static void GSDumpReplayerCpuShutdown();
static void GSDumpReplayerCpuReset();
static void GSDumpReplayerCpuStep();
static void GSDumpReplayerCpuExecute();
static void GSDumpReplayerExitExecution();
static void GSDumpReplayerCancelInstruction();
static void GSDumpReplayerCpuClear(u32 addr, u32 size);
static std::unique_ptr<GSDumpFile> s_dump_file;
static u32 s_current_packet = 0;
static u32 s_dump_frame_number = 0;
static s32 s_dump_loop_count = 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;
static bool s_is_dump_runner = false;
R5900cpu GSDumpReplayerCpu = {
GSDumpReplayerCpuReserve,
GSDumpReplayerCpuShutdown,
GSDumpReplayerCpuReset,
GSDumpReplayerCpuStep,
GSDumpReplayerCpuExecute,
GSDumpReplayerExitExecution,
GSDumpReplayerCancelInstruction,
GSDumpReplayerCpuClear};
static InterpVU0 gsDumpVU0;
static InterpVU1 gsDumpVU1;
bool GSDumpReplayer::IsReplayingDump()
{
return static_cast<bool>(s_dump_file);
}
bool GSDumpReplayer::IsRunner()
{
return s_is_dump_runner;
}
void GSDumpReplayer::SetIsDumpRunner(bool is_runner)
{
s_is_dump_runner = is_runner;
}
void GSDumpReplayer::SetLoopCount(s32 loop_count)
{
s_dump_loop_count = loop_count - 1;
}
int GSDumpReplayer::GetLoopCount()
{
return s_dump_loop_count;
}
bool GSDumpReplayer::Initialize(const char* filename)
{
Common::Timer timer;
Console.WriteLn("(GSDumpReplayer) Reading file '%s'...", filename);
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;
// loop infinitely by default
s_dump_loop_count = -1;
return true;
}
bool GSDumpReplayer::ChangeDump(const char* filename)
{
Console.WriteLn("(GSDumpReplayer) Switching to '%s'...", filename);
if (!VMManager::IsGSDumpFileName(filename))
{
Host::ReportFormattedErrorAsync("GSDumpReplayer", "'%s' is not a GS dump.", filename);
return false;
}
std::unique_ptr<GSDumpFile> new_dump(GSDumpFile::OpenGSDump(filename));
if (!new_dump || !new_dump->ReadFile())
{
Host::ReportFormattedErrorAsync("GSDumpReplayer", "Failed to open or read '%s'.", filename);
return false;
}
s_dump_file = std::move(new_dump);
s_current_packet = 0;
// Don't forget to reset the GS!
GSDumpReplayerCpuReset();
return true;
}
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();
}
u32 GSDumpReplayer::GetFrameNumber()
{
return s_dump_frame_number;
}
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};
MTGS::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 * VMManager::GetTargetSpeed());
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;
if (s_dump_loop_count > 0)
s_dump_loop_count--;
else if (s_dump_loop_count == 0)
{
Host::RequestVMShutdown(false, false, false);
s_dump_running = false;
}
}
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 + 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),
packet.data, packet.length);
}
break;
default:
break;
}
break;
}
case GSDumpTypes::GSType::VSync:
{
s_dump_frame_number++;
GSDumpReplayerUpdateFrameLimit();
GSDumpReplayerFrameLimit();
MTGS::PostVsyncStart(false);
VMManager::Internal::VSyncOnCPUThread();
if (VMManager::Internal::IsExecutionInterrupted())
GSDumpReplayerExitExecution();
}
break;
case GSDumpTypes::GSType::ReadFIFO2:
{
u32 size;
std::memcpy(&size, packet.data, sizeof(size));
// Allocate an extra quadword, some transfers write too much (e.g. Lego Racers 2 with Z24 downloads).
std::unique_ptr<u8[]> arr(new u8[(size + 1) * 16]);
MTGS::InitAndReadFIFO(arr.get(), size);
}
break;
case GSDumpTypes::GSType::Registers:
{
std::memcpy(PS2MEM_GS, packet.data, 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 GSDumpReplayerExitExecution()
{
s_dump_running = false;
}
void GSDumpReplayerCancelInstruction()
{
}
void GSDumpReplayerCpuClear(u32 addr, u32 size)
{
}
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();
std::string text;
ImVec2 text_size;
text.reserve(128);
#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)
fmt::format_to(std::back_inserter(text), "Dump Frame: {}", s_dump_frame_number);
DRAW_LINE(font, text.c_str(), IM_COL32(255, 255, 255, 255));
text.clear();
fmt::format_to(std::back_inserter(text), "Packet Number: {}/{}", 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
}