GPUDump: Add GPU dump recording and playback

Implements the specification from:

https://github.com/ps1dev/standards/blob/main/GPUDUMP.md
This commit is contained in:
Stenzek 2024-10-20 22:02:24 +10:00
parent 428c3e3426
commit 4ab22921c4
No known key found for this signature in database
30 changed files with 1450 additions and 188 deletions

View File

@ -47,6 +47,8 @@ add_library(core
gpu_backend.cpp gpu_backend.cpp
gpu_backend.h gpu_backend.h
gpu_commands.cpp gpu_commands.cpp
gpu_dump.cpp
gpu_dump.h
gpu_hw.cpp gpu_hw.cpp
gpu_hw.h gpu_hw.h
gpu_hw_shadergen.cpp gpu_hw_shadergen.cpp

View File

@ -46,6 +46,7 @@
<ClCompile Include="gdb_server.cpp" /> <ClCompile Include="gdb_server.cpp" />
<ClCompile Include="gpu_backend.cpp" /> <ClCompile Include="gpu_backend.cpp" />
<ClCompile Include="gpu_commands.cpp" /> <ClCompile Include="gpu_commands.cpp" />
<ClCompile Include="gpu_dump.cpp" />
<ClCompile Include="gpu_hw_shadergen.cpp" /> <ClCompile Include="gpu_hw_shadergen.cpp" />
<ClCompile Include="gpu_hw_texture_cache.cpp" /> <ClCompile Include="gpu_hw_texture_cache.cpp" />
<ClCompile Include="gpu_shadergen.cpp" /> <ClCompile Include="gpu_shadergen.cpp" />
@ -124,6 +125,7 @@
<ClInclude Include="game_list.h" /> <ClInclude Include="game_list.h" />
<ClInclude Include="gdb_server.h" /> <ClInclude Include="gdb_server.h" />
<ClInclude Include="gpu_backend.h" /> <ClInclude Include="gpu_backend.h" />
<ClInclude Include="gpu_dump.h" />
<ClInclude Include="gpu_hw_shadergen.h" /> <ClInclude Include="gpu_hw_shadergen.h" />
<ClInclude Include="gpu_hw_texture_cache.h" /> <ClInclude Include="gpu_hw_texture_cache.h" />
<ClInclude Include="gpu_shadergen.h" /> <ClInclude Include="gpu_shadergen.h" />

View File

@ -68,6 +68,7 @@
<ClCompile Include="gpu_sw_rasterizer.cpp" /> <ClCompile Include="gpu_sw_rasterizer.cpp" />
<ClCompile Include="gpu_hw_texture_cache.cpp" /> <ClCompile Include="gpu_hw_texture_cache.cpp" />
<ClCompile Include="memory_scanner.cpp" /> <ClCompile Include="memory_scanner.cpp" />
<ClCompile Include="gpu_dump.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="types.h" /> <ClInclude Include="types.h" />
@ -142,6 +143,7 @@
<ClInclude Include="gpu_sw_rasterizer.h" /> <ClInclude Include="gpu_sw_rasterizer.h" />
<ClInclude Include="gpu_hw_texture_cache.h" /> <ClInclude Include="gpu_hw_texture_cache.h" />
<ClInclude Include="memory_scanner.h" /> <ClInclude Include="memory_scanner.h" />
<ClInclude Include="gpu_dump.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="gpu_sw_rasterizer.inl" /> <None Include="gpu_sw_rasterizer.inl" />

View File

@ -2492,6 +2492,11 @@ void CPU::ExecuteInterpreter()
} }
} }
fastjmp_buf* CPU::GetExecutionJmpBuf()
{
return &s_jmp_buf;
}
void CPU::Execute() void CPU::Execute()
{ {
CheckForExecutionModeChange(); CheckForExecutionModeChange();

View File

@ -2,9 +2,12 @@
// SPDX-License-Identifier: CC-BY-NC-ND-4.0 // SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once #pragma once
#include "bus.h" #include "bus.h"
#include "cpu_core.h" #include "cpu_core.h"
struct fastjmp_buf;
namespace CPU { namespace CPU {
void SetPC(u32 new_pc); void SetPC(u32 new_pc);
@ -28,6 +31,9 @@ ALWAYS_INLINE static void CheckForPendingInterrupt()
void DispatchInterrupt(); void DispatchInterrupt();
// access to execution jump buffer, use with care!
fastjmp_buf* GetExecutionJmpBuf();
// icache stuff // icache stuff
ALWAYS_INLINE static bool IsCachedAddress(VirtualMemoryAddress address) ALWAYS_INLINE static bool IsCachedAddress(VirtualMemoryAddress address)
{ {

View File

@ -6,12 +6,14 @@
#include "cdrom.h" #include "cdrom.h"
#include "cpu_core.h" #include "cpu_core.h"
#include "gpu.h" #include "gpu.h"
#include "gpu_dump.h"
#include "imgui.h" #include "imgui.h"
#include "interrupt_controller.h" #include "interrupt_controller.h"
#include "mdec.h" #include "mdec.h"
#include "pad.h" #include "pad.h"
#include "spu.h" #include "spu.h"
#include "system.h" #include "system.h"
#include "timing_event.h"
#include "util/imgui_manager.h" #include "util/imgui_manager.h"
#include "util/state_wrapper.h" #include "util/state_wrapper.h"
@ -802,6 +804,28 @@ TickCount DMA::TransferMemoryToDevice(u32 address, u32 increment, u32 word_count
{ {
if (g_gpu->BeginDMAWrite()) [[likely]] if (g_gpu->BeginDMAWrite()) [[likely]]
{ {
if (GPUDump::Recorder* dump = g_gpu->GetGPUDump()) [[unlikely]]
{
// No wraparound?
dump->BeginGP0Packet(word_count);
if (((address + (increment * (word_count - 1))) & mask) >= address) [[likely]]
{
dump->WriteWords(reinterpret_cast<const u32*>(&Bus::g_ram[address]), word_count);
}
else
{
u32 dump_address = address;
for (u32 i = 0; i < word_count; i++)
{
u32 value;
std::memcpy(&value, &Bus::g_ram[dump_address], sizeof(u32));
dump->WriteWord(value);
dump_address = (dump_address + increment) & mask;
}
}
dump->EndGP0Packet();
}
u8* ram_pointer = Bus::g_ram; u8* ram_pointer = Bus::g_ram;
for (u32 i = 0; i < word_count; i++) for (u32 i = 0; i < word_count; i++)
{ {

View File

@ -216,7 +216,7 @@ std::string GameDatabase::GetSerialForPath(const char* path)
{ {
std::string ret; std::string ret;
if (System::IsLoadableFilename(path) && !System::IsExeFileName(path) && !System::IsPsfFileName(path)) if (System::IsLoadablePath(path) && !System::IsExePath(path) && !System::IsPsfPath(path))
{ {
std::unique_ptr<CDImage> image(CDImage::Open(path, false, nullptr)); std::unique_ptr<CDImage> image(CDImage::Open(path, false, nullptr));
if (image) if (image)

View File

@ -165,7 +165,7 @@ bool GameList::IsScannableFilename(std::string_view path)
if (StringUtil::EndsWithNoCase(path, ".bin")) if (StringUtil::EndsWithNoCase(path, ".bin"))
return false; return false;
return System::IsLoadableFilename(path); return System::IsLoadablePath(path);
} }
bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry) bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry)
@ -317,9 +317,9 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry)
bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry) bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry)
{ {
if (System::IsExeFileName(path)) if (System::IsExePath(path))
return GetExeListEntry(path, entry); return GetExeListEntry(path, entry);
if (System::IsPsfFileName(path.c_str())) if (System::IsPsfPath(path.c_str()))
return GetPsfListEntry(path, entry); return GetPsfListEntry(path, entry);
return GetDiscListEntry(path, entry); return GetDiscListEntry(path, entry);
} }

View File

@ -3,6 +3,7 @@
#include "gpu.h" #include "gpu.h"
#include "dma.h" #include "dma.h"
#include "gpu_dump.h"
#include "gpu_shadergen.h" #include "gpu_shadergen.h"
#include "gpu_sw_rasterizer.h" #include "gpu_sw_rasterizer.h"
#include "host.h" #include "host.h"
@ -10,6 +11,7 @@
#include "settings.h" #include "settings.h"
#include "system.h" #include "system.h"
#include "timers.h" #include "timers.h"
#include "timing_event.h"
#include "util/gpu_device.h" #include "util/gpu_device.h"
#include "util/image.h" #include "util/image.h"
@ -72,6 +74,7 @@ static bool CompressAndWriteTextureToFile(u32 width, u32 height, std::string fil
u8 quality, bool clear_alpha, bool flip_y, std::vector<u32> texture_data, u8 quality, bool clear_alpha, bool flip_y, std::vector<u32> texture_data,
u32 texture_data_stride, GPUTexture::Format texture_format, u32 texture_data_stride, GPUTexture::Format texture_format,
bool display_osd_message, bool use_thread); bool display_osd_message, bool use_thread);
static void RemoveSelfFromScreenshotThreads();
static void JoinScreenshotThreads(); static void JoinScreenshotThreads();
GPU::GPU() GPU::GPU()
@ -86,6 +89,7 @@ GPU::~GPU()
s_crtc_tick_event.Deactivate(); s_crtc_tick_event.Deactivate();
s_frame_done_event.Deactivate(); s_frame_done_event.Deactivate();
StopRecordingGPUDump();
JoinScreenshotThreads(); JoinScreenshotThreads();
DestroyDeinterlaceTextures(); DestroyDeinterlaceTextures();
g_gpu_device->RecycleTexture(std::move(m_chroma_smoothing_texture)); g_gpu_device->RecycleTexture(std::move(m_chroma_smoothing_texture));
@ -93,9 +97,11 @@ GPU::~GPU()
bool GPU::Initialize() bool GPU::Initialize()
{ {
if (!System::IsReplayingGPUDump())
s_crtc_tick_event.Activate();
m_force_progressive_scan = (g_settings.display_deinterlacing_mode == DisplayDeinterlacingMode::Progressive); m_force_progressive_scan = (g_settings.display_deinterlacing_mode == DisplayDeinterlacingMode::Progressive);
m_force_frame_timings = g_settings.gpu_force_video_timing; m_force_frame_timings = g_settings.gpu_force_video_timing;
s_crtc_tick_event.Activate();
m_fifo_size = g_settings.gpu_fifo_size; m_fifo_size = g_settings.gpu_fifo_size;
m_max_run_ahead = g_settings.gpu_max_run_ahead; m_max_run_ahead = g_settings.gpu_max_run_ahead;
m_console_is_pal = System::IsPALRegion(); m_console_is_pal = System::IsPALRegion();
@ -226,7 +232,7 @@ void GPU::SoftReset()
m_GPUSTAT.display_area_color_depth_24 = false; m_GPUSTAT.display_area_color_depth_24 = false;
m_GPUSTAT.vertical_interlace = false; m_GPUSTAT.vertical_interlace = false;
m_GPUSTAT.display_disable = true; m_GPUSTAT.display_disable = true;
m_GPUSTAT.dma_direction = DMADirection::Off; m_GPUSTAT.dma_direction = GPUDMADirection::Off;
m_drawing_area = {}; m_drawing_area = {};
m_drawing_area_changed = true; m_drawing_area_changed = true;
m_drawing_offset = {}; m_drawing_offset = {};
@ -420,19 +426,19 @@ void GPU::UpdateDMARequest()
bool dma_request; bool dma_request;
switch (m_GPUSTAT.dma_direction) switch (m_GPUSTAT.dma_direction)
{ {
case DMADirection::Off: case GPUDMADirection::Off:
dma_request = false; dma_request = false;
break; break;
case DMADirection::FIFO: case GPUDMADirection::FIFO:
dma_request = m_GPUSTAT.ready_to_recieve_dma; dma_request = m_GPUSTAT.ready_to_recieve_dma;
break; break;
case DMADirection::CPUtoGP0: case GPUDMADirection::CPUtoGP0:
dma_request = m_GPUSTAT.ready_to_recieve_dma; dma_request = m_GPUSTAT.ready_to_recieve_dma;
break; break;
case DMADirection::GPUREADtoCPU: case GPUDMADirection::GPUREADtoCPU:
dma_request = m_GPUSTAT.ready_to_send_vram; dma_request = m_GPUSTAT.ready_to_send_vram;
break; break;
@ -479,23 +485,35 @@ void GPU::WriteRegister(u32 offset, u32 value)
switch (offset) switch (offset)
{ {
case 0x00: case 0x00:
{
if (m_gpu_dump) [[unlikely]]
m_gpu_dump->WriteGP0Packet(value);
m_fifo.Push(value); m_fifo.Push(value);
ExecuteCommands(); ExecuteCommands();
return; return;
}
case 0x04: case 0x04:
{
if (m_gpu_dump) [[unlikely]]
m_gpu_dump->WriteGP1Packet(value);
WriteGP1(value); WriteGP1(value);
return; return;
}
default: default:
{
ERROR_LOG("Unhandled register write: {:02X} <- {:08X}", offset, value); ERROR_LOG("Unhandled register write: {:02X} <- {:08X}", offset, value);
return; return;
} }
} }
}
void GPU::DMARead(u32* words, u32 word_count) void GPU::DMARead(u32* words, u32 word_count)
{ {
if (m_GPUSTAT.dma_direction != DMADirection::GPUREADtoCPU) if (m_GPUSTAT.dma_direction != GPUDMADirection::GPUREADtoCPU)
{ {
ERROR_LOG("Invalid DMA direction from GPU DMA read"); ERROR_LOG("Invalid DMA direction from GPU DMA read");
std::fill_n(words, word_count, UINT32_C(0xFFFFFFFF)); std::fill_n(words, word_count, UINT32_C(0xFFFFFFFF));
@ -877,6 +895,11 @@ TickCount GPU::GetPendingCommandTicks() const
return SystemTicksToGPUTicks(s_command_tick_event.GetTicksSinceLastExecution()); return SystemTicksToGPUTicks(s_command_tick_event.GetTicksSinceLastExecution());
} }
TickCount GPU::GetRemainingCommandTicks() const
{
return std::max<TickCount>(m_pending_command_ticks - GetPendingCommandTicks(), 0);
}
void GPU::UpdateCRTCTickEvent() void GPU::UpdateCRTCTickEvent()
{ {
// figure out how many GPU ticks until the next vblank or event // figure out how many GPU ticks until the next vblank or event
@ -931,6 +954,7 @@ void GPU::UpdateCRTCTickEvent()
ticks_until_event = std::min(ticks_until_event, ticks_until_hblank_start_or_end); ticks_until_event = std::min(ticks_until_event, ticks_until_hblank_start_or_end);
} }
if (!System::IsReplayingGPUDump()) [[likely]]
s_crtc_tick_event.Schedule(CRTCTicksToSystemTicks(ticks_until_event, m_crtc_state.fractional_ticks)); s_crtc_tick_event.Schedule(CRTCTicksToSystemTicks(ticks_until_event, m_crtc_state.fractional_ticks));
} }
@ -1030,6 +1054,13 @@ void GPU::CRTCTickEvent(TickCount ticks)
{ {
DEBUG_LOG("Now in v-blank"); DEBUG_LOG("Now in v-blank");
if (m_gpu_dump) [[unlikely]]
{
m_gpu_dump->WriteVSync(System::GetGlobalTickCounter());
if (m_gpu_dump->IsFinished()) [[unlikely]]
StopRecordingGPUDump();
}
// flush any pending draws and "scan out" the image // flush any pending draws and "scan out" the image
// TODO: move present in here I guess // TODO: move present in here I guess
FlushRender(); FlushRender();
@ -1273,7 +1304,7 @@ void GPU::WriteGP1(u32 value)
const u32 param = value & UINT32_C(0x00FFFFFF); const u32 param = value & UINT32_C(0x00FFFFFF);
switch (command) switch (command)
{ {
case 0x00: // Reset GPU case static_cast<u8>(GP1Command::ResetGPU):
{ {
DEBUG_LOG("GP1 reset GPU"); DEBUG_LOG("GP1 reset GPU");
s_command_tick_event.InvokeEarly(); s_command_tick_event.InvokeEarly();
@ -1282,7 +1313,7 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x01: // Clear FIFO case static_cast<u8>(GP1Command::ClearFIFO):
{ {
DEBUG_LOG("GP1 clear FIFO"); DEBUG_LOG("GP1 clear FIFO");
s_command_tick_event.InvokeEarly(); s_command_tick_event.InvokeEarly();
@ -1305,7 +1336,7 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x02: // Acknowledge Interrupt case static_cast<u8>(GP1Command::AcknowledgeInterrupt):
{ {
DEBUG_LOG("Acknowledge interrupt"); DEBUG_LOG("Acknowledge interrupt");
m_GPUSTAT.interrupt_request = false; m_GPUSTAT.interrupt_request = false;
@ -1313,7 +1344,7 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x03: // Display on/off case static_cast<u8>(GP1Command::SetDisplayDisable):
{ {
const bool disable = ConvertToBoolUnchecked(value & 0x01); const bool disable = ConvertToBoolUnchecked(value & 0x01);
DEBUG_LOG("Display {}", disable ? "disabled" : "enabled"); DEBUG_LOG("Display {}", disable ? "disabled" : "enabled");
@ -1326,18 +1357,18 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x04: // DMA Direction case static_cast<u8>(GP1Command::SetDMADirection):
{ {
DEBUG_LOG("DMA direction <- 0x{:02X}", static_cast<u32>(param)); DEBUG_LOG("DMA direction <- 0x{:02X}", static_cast<u32>(param));
if (m_GPUSTAT.dma_direction != static_cast<DMADirection>(param)) if (m_GPUSTAT.dma_direction != static_cast<GPUDMADirection>(param))
{ {
m_GPUSTAT.dma_direction = static_cast<DMADirection>(param); m_GPUSTAT.dma_direction = static_cast<GPUDMADirection>(param);
UpdateDMARequest(); UpdateDMARequest();
} }
} }
break; break;
case 0x05: // Set display start address case static_cast<u8>(GP1Command::SetDisplayStartAddress):
{ {
const u32 new_value = param & CRTCState::Regs::DISPLAY_ADDRESS_START_MASK; const u32 new_value = param & CRTCState::Regs::DISPLAY_ADDRESS_START_MASK;
DEBUG_LOG("Display address start <- 0x{:08X}", new_value); DEBUG_LOG("Display address start <- 0x{:08X}", new_value);
@ -1353,7 +1384,7 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x06: // Set horizontal display range case static_cast<u8>(GP1Command::SetHorizontalDisplayRange):
{ {
const u32 new_value = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK; const u32 new_value = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK;
DEBUG_LOG("Horizontal display range <- 0x{:08X}", new_value); DEBUG_LOG("Horizontal display range <- 0x{:08X}", new_value);
@ -1367,7 +1398,7 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x07: // Set vertical display range case static_cast<u8>(GP1Command::SetVerticalDisplayRange):
{ {
const u32 new_value = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK; const u32 new_value = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK;
DEBUG_LOG("Vertical display range <- 0x{:08X}", new_value); DEBUG_LOG("Vertical display range <- 0x{:08X}", new_value);
@ -1381,22 +1412,9 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x08: // Set display mode case static_cast<u8>(GP1Command::SetDisplayMode):
{ {
union GP1_08h const GP1SetDisplayMode dm{param};
{
u32 bits;
BitField<u32, u8, 0, 2> horizontal_resolution_1;
BitField<u32, bool, 2, 1> vertical_resolution;
BitField<u32, bool, 3, 1> pal_mode;
BitField<u32, bool, 4, 1> display_area_color_depth;
BitField<u32, bool, 5, 1> vertical_interlace;
BitField<u32, bool, 6, 1> horizontal_resolution_2;
BitField<u32, bool, 7, 1> reverse_flag;
};
const GP1_08h dm{param};
GPUSTAT new_GPUSTAT{m_GPUSTAT.bits}; GPUSTAT new_GPUSTAT{m_GPUSTAT.bits};
new_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1; new_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1;
new_GPUSTAT.vertical_resolution = dm.vertical_resolution; new_GPUSTAT.vertical_resolution = dm.vertical_resolution;
@ -1425,7 +1443,7 @@ void GPU::WriteGP1(u32 value)
} }
break; break;
case 0x09: // Allow texture disable case static_cast<u8>(GP1Command::SetAllowTextureDisable):
{ {
m_set_texture_disable_mask = ConvertToBoolUnchecked(param & 0x01); m_set_texture_disable_mask = ConvertToBoolUnchecked(param & 0x01);
DEBUG_LOG("Set texture disable mask <- {}", m_set_texture_disable_mask ? "allowed" : "ignored"); DEBUG_LOG("Set texture disable mask <- {}", m_set_texture_disable_mask ? "allowed" : "ignored");
@ -2471,20 +2489,7 @@ bool CompressAndWriteTextureToFile(u32 width, u32 height, std::string filename,
} }
if (use_thread) if (use_thread)
{ RemoveSelfFromScreenshotThreads();
// remove ourselves from the list, if the GS thread is waiting for us, we won't be in there
const auto this_id = std::this_thread::get_id();
std::unique_lock lock(s_screenshot_threads_mutex);
for (auto it = s_screenshot_threads.begin(); it != s_screenshot_threads.end(); ++it)
{
if (it->get_id() == this_id)
{
it->detach();
s_screenshot_threads.erase(it);
break;
}
}
}
return result; return result;
}; };
@ -2502,6 +2507,21 @@ bool CompressAndWriteTextureToFile(u32 width, u32 height, std::string filename,
return true; return true;
} }
void RemoveSelfFromScreenshotThreads()
{
const auto this_id = std::this_thread::get_id();
std::unique_lock lock(s_screenshot_threads_mutex);
for (auto it = s_screenshot_threads.begin(); it != s_screenshot_threads.end(); ++it)
{
if (it->get_id() == this_id)
{
it->detach();
s_screenshot_threads.erase(it);
break;
}
}
}
void JoinScreenshotThreads() void JoinScreenshotThreads()
{ {
std::unique_lock lock(s_screenshot_threads_mutex); std::unique_lock lock(s_screenshot_threads_mutex);
@ -2886,3 +2906,209 @@ void GPU::UpdateStatistics(u32 frame_count)
ResetStatistics(); ResetStatistics();
} }
bool GPU::StartRecordingGPUDump(const char* path, u32 num_frames /* = 1 */)
{
if (m_gpu_dump)
StopRecordingGPUDump();
// if we're not dumping forever, compute the frame count based on the internal fps
// +1 because we want to actually see the buffer swap...
if (num_frames != 0)
{
num_frames = std::max(num_frames, static_cast<u32>(static_cast<float>(num_frames + 1) *
std::ceil(System::GetVPS() / System::GetFPS())));
}
// ensure vram is up to date
ReadVRAM(0, 0, VRAM_WIDTH, VRAM_HEIGHT);
std::string osd_key = fmt::format("GPUDump_{}", Path::GetFileName(path));
Error error;
m_gpu_dump = GPUDump::Recorder::Create(path, System::GetGameSerial(), num_frames, &error);
if (!m_gpu_dump)
{
Host::AddIconOSDWarning(
std::move(osd_key), ICON_EMOJI_CAMERA_WITH_FLASH,
fmt::format("{}\n{}", TRANSLATE_SV("GPU", "Failed to start GPU trace:"), error.GetDescription()),
Host::OSD_ERROR_DURATION);
return false;
}
Host::AddIconOSDMessage(
std::move(osd_key), ICON_EMOJI_CAMERA_WITH_FLASH,
(num_frames != 0) ?
fmt::format(TRANSLATE_FS("GPU", "Saving {0} frame GPU trace to '{1}'."), num_frames, Path::GetFileName(path)) :
fmt::format(TRANSLATE_FS("GPU", "Saving multi-frame frame GPU trace to '{1}'."), num_frames,
Path::GetFileName(path)),
Host::OSD_QUICK_DURATION);
// save screenshot to same location to identify it
RenderScreenshotToFile(Path::ReplaceExtension(path, "png"), DisplayScreenshotMode::ScreenResolution, 85, true, false);
return true;
}
void GPU::StopRecordingGPUDump()
{
if (!m_gpu_dump)
return;
Error error;
if (!m_gpu_dump->Close(&error))
{
Host::AddIconOSDWarning(
"GPUDump", ICON_EMOJI_CAMERA_WITH_FLASH,
fmt::format("{}\n{}", TRANSLATE_SV("GPU", "Failed to close GPU trace:"), error.GetDescription()),
Host::OSD_ERROR_DURATION);
m_gpu_dump.reset();
}
// Are we compressing the dump?
const GPUDumpCompressionMode compress_mode =
Settings::ParseGPUDumpCompressionMode(Host::GetTinyStringSettingValue("GPU", "DumpCompressionMode"))
.value_or(Settings::DEFAULT_GPU_DUMP_COMPRESSION_MODE);
std::string osd_key = fmt::format("GPUDump_{}", Path::GetFileName(m_gpu_dump->GetPath()));
if (compress_mode == GPUDumpCompressionMode::Disabled)
{
Host::AddIconOSDMessage(
"GPUDump", ICON_EMOJI_CAMERA_WITH_FLASH,
fmt::format(TRANSLATE_FS("GPU", "Saved GPU trace to '{}'."), Path::GetFileName(m_gpu_dump->GetPath())),
Host::OSD_QUICK_DURATION);
m_gpu_dump.reset();
return;
}
std::string source_path = m_gpu_dump->GetPath();
m_gpu_dump.reset();
// Use a 60 second timeout to give it plenty of time to actually save.
Host::AddIconOSDMessage(
osd_key, ICON_EMOJI_CAMERA_WITH_FLASH,
fmt::format(TRANSLATE_FS("GPU", "Compressing GPU trace '{}'..."), Path::GetFileName(source_path)), 60.0f);
std::unique_lock screenshot_lock(s_screenshot_threads_mutex);
s_screenshot_threads.emplace_back(
[compress_mode, source_path = std::move(source_path), osd_key = std::move(osd_key)]() mutable {
Error error;
if (GPUDump::Recorder::Compress(source_path, compress_mode, &error))
{
Host::AddIconOSDMessage(
std::move(osd_key), ICON_EMOJI_CAMERA_WITH_FLASH,
fmt::format(TRANSLATE_FS("GPU", "Saved GPU trace to '{}'."), Path::GetFileName(source_path)),
Host::OSD_QUICK_DURATION);
}
else
{
Host::AddIconOSDWarning(
std::move(osd_key), ICON_EMOJI_CAMERA_WITH_FLASH,
fmt::format("{}\n{}",
SmallString::from_format(TRANSLATE_FS("GPU", "Failed to save GPU trace to '{}':"),
Path::GetFileName(source_path)),
error.GetDescription()),
Host::OSD_ERROR_DURATION);
}
RemoveSelfFromScreenshotThreads();
});
}
void GPU::WriteCurrentVideoModeToDump(GPUDump::Recorder* dump) const
{
// display disable
dump->WriteGP1Command(GP1Command::SetDisplayDisable, BoolToUInt32(m_GPUSTAT.display_disable));
dump->WriteGP1Command(GP1Command::SetDisplayStartAddress, m_crtc_state.regs.display_address_start);
dump->WriteGP1Command(GP1Command::SetHorizontalDisplayRange, m_crtc_state.regs.horizontal_display_range);
dump->WriteGP1Command(GP1Command::SetVerticalDisplayRange, m_crtc_state.regs.vertical_display_range);
dump->WriteGP1Command(GP1Command::SetAllowTextureDisable, BoolToUInt32(m_set_texture_disable_mask));
// display mode
GP1SetDisplayMode dispmode = {};
dispmode.horizontal_resolution_1 = m_GPUSTAT.horizontal_resolution_1.GetValue();
dispmode.vertical_resolution = m_GPUSTAT.vertical_resolution.GetValue();
dispmode.pal_mode = m_GPUSTAT.pal_mode.GetValue();
dispmode.display_area_color_depth = m_GPUSTAT.display_area_color_depth_24.GetValue();
dispmode.vertical_interlace = m_GPUSTAT.vertical_interlace.GetValue();
dispmode.horizontal_resolution_2 = m_GPUSTAT.horizontal_resolution_2.GetValue();
dispmode.reverse_flag = m_GPUSTAT.reverse_flag.GetValue();
dump->WriteGP1Command(GP1Command::SetDisplayMode, dispmode.bits);
}
void GPU::ProcessGPUDumpPacket(GPUDump::PacketType type, const std::span<const u32> data)
{
switch (type)
{
case GPUDump::PacketType::GPUPort0Data:
{
if (data.empty()) [[unlikely]]
{
WARNING_LOG("Empty GPU dump GP0 packet!");
return;
}
// ensure it doesn't block
m_pending_command_ticks = 0;
UpdateCommandTickEvent();
if (data.size() == 1) [[unlikely]]
{
// direct GP0 write
WriteRegister(0, data[0]);
}
else
{
// don't overflow the fifo...
size_t current_word = 0;
while (current_word < data.size())
{
const u32 block_size = std::min(m_fifo_size - m_fifo.GetSize(), static_cast<u32>(data.size() - current_word));
if (block_size == 0)
{
ERROR_LOG("FIFO overflow while processing dump packet of {} words", data.size());
break;
}
for (u32 i = 0; i < block_size; i++)
m_fifo.Push(ZeroExtend64(data[current_word++]));
ExecuteCommands();
}
}
}
break;
case GPUDump::PacketType::GPUPort1Data:
{
if (data.size() != 1) [[unlikely]]
{
WARNING_LOG("Incorrectly-sized GPU dump GP1 packet: {} words", data.size());
return;
}
WriteRegister(4, data[0]);
}
break;
case GPUDump::PacketType::VSyncEvent:
{
// don't play silly buggers with events
m_pending_command_ticks = 0;
UpdateCommandTickEvent();
// we _should_ be using the tick count for the event, but it breaks with looping.
// instead, just add a fixed amount
const TickCount crtc_ticks_per_frame =
static_cast<TickCount>(m_crtc_state.horizontal_total) * static_cast<TickCount>(m_crtc_state.vertical_total);
const TickCount system_ticks_per_frame =
CRTCTicksToSystemTicks(crtc_ticks_per_frame, m_crtc_state.fractional_ticks);
SystemTicksToCRTCTicks(system_ticks_per_frame, &m_crtc_state.fractional_ticks);
TimingEvents::SetGlobalTickCounter(TimingEvents::GetGlobalTickCounter() +
static_cast<GlobalTicks>(system_ticks_per_frame));
FlushRender();
UpdateDisplay();
System::FrameDone();
}
break;
default:
break;
}
}

View File

@ -5,7 +5,6 @@
#include "gpu_types.h" #include "gpu_types.h"
#include "timers.h" #include "timers.h"
#include "timing_event.h"
#include "types.h" #include "types.h"
#include "util/gpu_device.h" #include "util/gpu_device.h"
@ -20,9 +19,11 @@
#include <deque> #include <deque>
#include <memory> #include <memory>
#include <string> #include <string>
#include <span>
#include <tuple> #include <tuple>
#include <vector> #include <vector>
class Error;
class SmallStringBase; class SmallStringBase;
class StateWrapper; class StateWrapper;
@ -32,6 +33,11 @@ class GPUTexture;
class GPUPipeline; class GPUPipeline;
class MediaCapture; class MediaCapture;
namespace GPUDump {
enum class PacketType : u8;
class Recorder;
class Player;
}
struct Settings; struct Settings;
namespace Threading { namespace Threading {
@ -49,14 +55,6 @@ public:
DrawingPolyLine DrawingPolyLine
}; };
enum class DMADirection : u32
{
Off = 0,
FIFO = 1,
CPUtoGP0 = 2,
GPUREADtoCPU = 3
};
enum : u32 enum : u32
{ {
MAX_FIFO_SIZE = 4096, MAX_FIFO_SIZE = 4096,
@ -120,7 +118,7 @@ public:
ALWAYS_INLINE bool BeginDMAWrite() const ALWAYS_INLINE bool BeginDMAWrite() const
{ {
return (m_GPUSTAT.dma_direction == DMADirection::CPUtoGP0 || m_GPUSTAT.dma_direction == DMADirection::FIFO); return (m_GPUSTAT.dma_direction == GPUDMADirection::CPUtoGP0 || m_GPUSTAT.dma_direction == GPUDMADirection::FIFO);
} }
ALWAYS_INLINE void DMAWrite(u32 address, u32 value) ALWAYS_INLINE void DMAWrite(u32 address, u32 value)
{ {
@ -128,6 +126,13 @@ public:
} }
void EndDMAWrite(); void EndDMAWrite();
/// Writing to GPU dump.
GPUDump::Recorder* GetGPUDump() const { return m_gpu_dump.get(); }
bool StartRecordingGPUDump(const char* path, u32 num_frames = 1);
void StopRecordingGPUDump();
void WriteCurrentVideoModeToDump(GPUDump::Recorder* dump) const;
void ProcessGPUDumpPacket(GPUDump::PacketType type, const std::span<const u32> data);
/// Returns true if no data is being sent from VRAM to the DAC or that no portion of VRAM would be visible on screen. /// Returns true if no data is being sent from VRAM to the DAC or that no portion of VRAM would be visible on screen.
ALWAYS_INLINE bool IsDisplayDisabled() const ALWAYS_INLINE bool IsDisplayDisabled() const
{ {
@ -152,6 +157,7 @@ public:
/// Returns the number of pending GPU ticks. /// Returns the number of pending GPU ticks.
TickCount GetPendingCRTCTicks() const; TickCount GetPendingCRTCTicks() const;
TickCount GetPendingCommandTicks() const; TickCount GetPendingCommandTicks() const;
TickCount GetRemainingCommandTicks() const;
/// Returns true if enough ticks have passed for the raster to be on the next line. /// Returns true if enough ticks have passed for the raster to be on the next line.
bool IsCRTCScanlinePending() const; bool IsCRTCScanlinePending() const;
@ -414,54 +420,7 @@ protected:
AddCommandTicks(std::max(drawn_width, drawn_height)); AddCommandTicks(std::max(drawn_width, drawn_height));
} }
union GPUSTAT GPUSTAT m_GPUSTAT = {};
{
// During transfer/render operations, if ((dst_pixel & mask_and) == 0) { pixel = src_pixel | mask_or }
u32 bits;
BitField<u32, u8, 0, 4> texture_page_x_base;
BitField<u32, u8, 4, 1> texture_page_y_base;
BitField<u32, GPUTransparencyMode, 5, 2> semi_transparency_mode;
BitField<u32, GPUTextureMode, 7, 2> texture_color_mode;
BitField<u32, bool, 9, 1> dither_enable;
BitField<u32, bool, 10, 1> draw_to_displayed_field;
BitField<u32, bool, 11, 1> set_mask_while_drawing;
BitField<u32, bool, 12, 1> check_mask_before_draw;
BitField<u32, u8, 13, 1> interlaced_field;
BitField<u32, bool, 14, 1> reverse_flag;
BitField<u32, bool, 15, 1> texture_disable;
BitField<u32, u8, 16, 1> horizontal_resolution_2;
BitField<u32, u8, 17, 2> horizontal_resolution_1;
BitField<u32, bool, 19, 1> vertical_resolution;
BitField<u32, bool, 20, 1> pal_mode;
BitField<u32, bool, 21, 1> display_area_color_depth_24;
BitField<u32, bool, 22, 1> vertical_interlace;
BitField<u32, bool, 23, 1> display_disable;
BitField<u32, bool, 24, 1> interrupt_request;
BitField<u32, bool, 25, 1> dma_data_request;
BitField<u32, bool, 26, 1> gpu_idle;
BitField<u32, bool, 27, 1> ready_to_send_vram;
BitField<u32, bool, 28, 1> ready_to_recieve_dma;
BitField<u32, DMADirection, 29, 2> dma_direction;
BitField<u32, bool, 31, 1> display_line_lsb;
ALWAYS_INLINE bool IsMaskingEnabled() const
{
static constexpr u32 MASK = ((1 << 11) | (1 << 12));
return ((bits & MASK) != 0);
}
ALWAYS_INLINE bool SkipDrawingToActiveField() const
{
static constexpr u32 MASK = (1 << 19) | (1 << 22) | (1 << 10);
static constexpr u32 ACTIVE = (1 << 19) | (1 << 22);
return ((bits & MASK) == ACTIVE);
}
ALWAYS_INLINE bool InInterleaved480iMode() const
{
static constexpr u32 ACTIVE = (1 << 19) | (1 << 22);
return ((bits & ACTIVE) == ACTIVE);
}
} m_GPUSTAT = {};
struct DrawMode struct DrawMode
{ {
@ -606,6 +565,8 @@ protected:
u32 m_blit_remaining_words; u32 m_blit_remaining_words;
GPURenderCommand m_render_command{}; GPURenderCommand m_render_command{};
std::unique_ptr<GPUDump::Recorder> m_gpu_dump;
ALWAYS_INLINE u32 FifoPop() { return Truncate32(m_fifo.Pop()); } ALWAYS_INLINE u32 FifoPop() { return Truncate32(m_fifo.Pop()); }
ALWAYS_INLINE u32 FifoPeek() { return Truncate32(m_fifo.Peek()); } ALWAYS_INLINE u32 FifoPeek() { return Truncate32(m_fifo.Peek()); }
ALWAYS_INLINE u32 FifoPeek(u32 i) { return Truncate32(m_fifo.Peek(i)); } ALWAYS_INLINE u32 FifoPeek(u32 i) { return Truncate32(m_fifo.Peek(i)); }

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: CC-BY-NC-ND-4.0 // SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gpu.h" #include "gpu.h"
#include "gpu_dump.h"
#include "gpu_hw_texture_cache.h" #include "gpu_hw_texture_cache.h"
#include "interrupt_controller.h" #include "interrupt_controller.h"
#include "system.h" #include "system.h"
@ -604,6 +605,11 @@ bool GPU::HandleCopyRectangleVRAMToCPUCommand()
m_counters.num_reads++; m_counters.num_reads++;
m_blitter_state = BlitterState::ReadingVRAM; m_blitter_state = BlitterState::ReadingVRAM;
m_command_total_words = 0; m_command_total_words = 0;
// toss the entire read in the recorded trace. we might want to change this to mirroring GPUREAD in the future..
if (m_gpu_dump) [[unlikely]]
m_gpu_dump->WriteDiscardVRAMRead(m_vram_transfer.width, m_vram_transfer.height);
return true; return true;
} }

534
src/core/gpu_dump.cpp Normal file
View File

@ -0,0 +1,534 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gpu_dump.h"
#include "cpu_core.h"
#include "cpu_core_private.h"
#include "gpu.h"
#include "settings.h"
#include "scmversion/scmversion.h"
#include "util/compress_helpers.h"
#include "common/align.h"
#include "common/assert.h"
#include "common/binary_reader_writer.h"
#include "common/error.h"
#include "common/fastjmp.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"
#include "common/timer.h"
#include "fmt/format.h"
LOG_CHANNEL(GPUDump);
namespace GPUDump {
static constexpr GPUVersion GPU_VERSION = GPUVersion::V2_1MB_VRAM;
// Write the file header.
static constexpr u8 FILE_HEADER[] = {'P', 'S', 'X', 'G', 'P', 'U', 'D', 'U', 'M', 'P', 'v', '1', '\0', '\0'};
}; // namespace GPUDump
GPUDump::Recorder::Recorder(FileSystem::AtomicRenamedFile fp, u32 vsyncs_remaining, std::string path)
: m_fp(std::move(fp)), m_vsyncs_remaining(vsyncs_remaining), m_path(path)
{
}
GPUDump::Recorder::~Recorder()
{
if (m_fp)
FileSystem::DiscardAtomicRenamedFile(m_fp);
}
bool GPUDump::Recorder::IsFinished()
{
if (m_vsyncs_remaining == 0)
return false;
m_vsyncs_remaining--;
return (m_vsyncs_remaining == 0);
}
bool GPUDump::Recorder::Close(Error* error)
{
if (m_write_error)
{
Error::SetStringView(error, "Previous write error occurred.");
return false;
}
return FileSystem::CommitAtomicRenamedFile(m_fp, error);
}
std::unique_ptr<GPUDump::Recorder> GPUDump::Recorder::Create(std::string path, std::string_view serial, u32 num_frames,
Error* error)
{
std::unique_ptr<Recorder> ret;
auto fp = FileSystem::CreateAtomicRenamedFile(path, error);
if (!fp)
return ret;
ret = std::unique_ptr<Recorder>(new Recorder(std::move(fp), num_frames, std::move(path)));
ret->WriteHeaders(serial);
g_gpu->WriteCurrentVideoModeToDump(ret.get());
ret->WriteCurrentVRAM();
// Write start of stream.
ret->BeginPacket(PacketType::TraceBegin);
ret->EndPacket();
if (ret->m_write_error)
{
Error::SetStringView(error, "Previous write error occurred.");
ret.reset();
}
return ret;
}
bool GPUDump::Recorder::Compress(const std::string& source_path, GPUDumpCompressionMode mode, Error* error)
{
if (mode == GPUDumpCompressionMode::Disabled)
return true;
std::optional<DynamicHeapArray<u8>> data = FileSystem::ReadBinaryFile(source_path.c_str(), error);
if (!data)
return false;
if (mode >= GPUDumpCompressionMode::ZstLow && mode <= GPUDumpCompressionMode::ZstHigh)
{
const int clevel =
((mode == GPUDumpCompressionMode::ZstLow) ? 1 : ((mode == GPUDumpCompressionMode::ZstHigh) ? 19 : 0));
if (!CompressHelpers::CompressToFile(fmt::format("{}.zst", source_path).c_str(), std::move(data.value()), clevel,
true, error))
{
return false;
}
}
else
{
Error::SetStringView(error, "Unknown compression mode.");
return false;
}
// remove original file
return FileSystem::DeleteFile(source_path.c_str(), error);
}
void GPUDump::Recorder::BeginGP0Packet(u32 size)
{
BeginPacket(PacketType::GPUPort0Data, size);
}
void GPUDump::Recorder::WriteGP0Packet(u32 word)
{
BeginGP0Packet(1);
WriteWord(word);
EndGP0Packet();
}
void GPUDump::Recorder::EndGP0Packet()
{
DebugAssert(!m_packet_buffer.empty());
EndPacket();
}
void GPUDump::Recorder::WriteGP1Packet(u32 value)
{
const u32 command = (value >> 24) & 0x3F;
// only in-range commands, no info
if (command > static_cast<u32>(GP1Command::SetAllowTextureDisable))
return;
// filter DMA direction, we don't want to screw with that
if (command == static_cast<u32>(GP1Command::SetDMADirection))
return;
WriteGP1Command(static_cast<GP1Command>(command), value & 0x00FFFFFFu);
}
void GPUDump::Recorder::WriteDiscardVRAMRead(u32 width, u32 height)
{
const u32 num_words = Common::AlignUpPow2(width * height * static_cast<u32>(sizeof(u16)), sizeof(u32)) / sizeof(u32);
if (num_words == 0)
return;
BeginPacket(GPUDump::PacketType::DiscardPort0Data, 1);
WriteWord(num_words);
EndPacket();
}
void GPUDump::Recorder::WriteVSync(u64 ticks)
{
BeginPacket(GPUDump::PacketType::VSyncEvent, 2);
WriteWord(static_cast<u32>(ticks));
WriteWord(static_cast<u32>(ticks >> 32));
EndPacket();
}
void GPUDump::Recorder::BeginPacket(PacketType packet, u32 minimum_size)
{
DebugAssert(m_packet_buffer.empty());
m_current_packet = packet;
m_packet_buffer.reserve(minimum_size);
}
void GPUDump::Recorder::WriteWords(const u32* words, size_t word_count)
{
Assert(((m_packet_buffer.size() + word_count) * sizeof(u32)) <= MAX_PACKET_LENGTH);
// we don't need the zeroing here...
const size_t current_offset = m_packet_buffer.size();
m_packet_buffer.resize(current_offset + word_count);
std::memcpy(&m_packet_buffer[current_offset], words, sizeof(u32) * word_count);
}
void GPUDump::Recorder::WriteWords(const std::span<const u32> words)
{
WriteWords(words.data(), words.size());
}
void GPUDump::Recorder::WriteString(std::string_view str)
{
const size_t aligned_length = Common::AlignDownPow2(str.length(), sizeof(u32));
for (size_t i = 0; i < aligned_length; i += sizeof(u32))
{
u32 word;
std::memcpy(&word, &str[i], sizeof(word));
WriteWord(word);
}
// zero termination and/or padding for last bytes
u8 pad_word[4] = {};
for (size_t i = aligned_length, pad_i = 0; i < str.length(); i++, pad_i++)
pad_word[pad_i] = str[i];
WriteWord(std::bit_cast<u32>(pad_word));
}
void GPUDump::Recorder::WriteBytes(const void* data, size_t data_size_in_bytes)
{
Assert(((m_packet_buffer.size() * sizeof(u32)) + data_size_in_bytes) <= MAX_PACKET_LENGTH);
const u32 num_words = Common::AlignUpPow2(static_cast<u32>(data_size_in_bytes), sizeof(u32)) / sizeof(u32);
const size_t current_offset = m_packet_buffer.size();
// NOTE: assumes resize() zeros it out
m_packet_buffer.resize(current_offset + num_words);
std::memcpy(&m_packet_buffer[current_offset], data, data_size_in_bytes);
}
void GPUDump::Recorder::EndPacket()
{
if (m_write_error)
return;
Assert(m_packet_buffer.size() <= MAX_PACKET_LENGTH);
PacketHeader hdr = {};
hdr.length = static_cast<u32>(m_packet_buffer.size());
hdr.type = m_current_packet;
if (std::fwrite(&hdr, sizeof(hdr), 1, m_fp.get()) != 1 ||
(!m_packet_buffer.empty() &&
std::fwrite(m_packet_buffer.data(), m_packet_buffer.size() * sizeof(u32), 1, m_fp.get()) != 1))
{
ERROR_LOG("Failed to write packet to file: {}", Error::CreateErrno(errno).GetDescription());
m_write_error = true;
m_packet_buffer.clear();
return;
}
m_packet_buffer.clear();
}
void GPUDump::Recorder::WriteGP1Command(GP1Command command, u32 param)
{
BeginPacket(PacketType::GPUPort1Data, 1);
WriteWord(((static_cast<u32>(command) & 0x3F) << 24) | (param & 0x00FFFFFFu));
EndPacket();
}
void GPUDump::Recorder::WriteHeaders(std::string_view serial)
{
if (std::fwrite(FILE_HEADER, sizeof(FILE_HEADER), 1, m_fp.get()) != 1)
{
ERROR_LOG("Failed to write file header: {}", Error::CreateErrno(errno).GetDescription());
m_write_error = true;
return;
}
// Write GPU version.
BeginPacket(PacketType::GPUVersion, 1);
WriteWord(static_cast<u32>(GPU_VERSION));
EndPacket();
// Write Game ID.
BeginPacket(PacketType::GameID);
WriteString(serial.empty() ? std::string_view("UNKNOWN") : serial);
EndPacket();
// Write textual video mode.
BeginPacket(PacketType::TextualVideoFormat);
WriteString(g_gpu->IsInPALMode() ? "PAL" : "NTSC");
EndPacket();
// Write DuckStation version.
BeginPacket(PacketType::Comment);
WriteString(
SmallString::from_format("Created by DuckStation {} for {}/{}.", g_scm_tag_str, TARGET_OS_STR, CPU_ARCH_STR));
EndPacket();
}
void GPUDump::Recorder::WriteCurrentVRAM()
{
BeginPacket(PacketType::GPUPort0Data, sizeof(u32) * 2 + (VRAM_SIZE / sizeof(u32)));
// command, coords, size. size is written as zero, for 1024x512
WriteWord(0xA0u << 24);
WriteWord(0);
WriteWord(0);
// actual vram data
WriteBytes(g_vram, VRAM_SIZE);
EndPacket();
}
GPUDump::Player::Player(std::string path, DynamicHeapArray<u8> data) : m_data(std::move(data)), m_path(std::move(path))
{
}
GPUDump::Player::~Player() = default;
std::unique_ptr<GPUDump::Player> GPUDump::Player::Open(std::string path, Error* error)
{
std::unique_ptr<Player> ret;
Common::Timer timer;
std::optional<DynamicHeapArray<u8>> data;
if (StringUtil::EndsWithNoCase(path, ".psxgpu.zst"))
data = CompressHelpers::DecompressFile(path.c_str(), std::nullopt, error);
else
data = FileSystem::ReadBinaryFile(path.c_str(), error);
if (!data.has_value())
return ret;
ret = std::unique_ptr<Player>(new Player(std::move(path), std::move(data.value())));
if (!ret->Preprocess(error))
{
ret.reset();
return ret;
}
INFO_LOG("Loading {} took {:.0f}ms.", Path::GetFileName(ret->GetPath()), timer.GetTimeMilliseconds());
return ret;
}
std::optional<GPUDump::Player::PacketRef> GPUDump::Player::GetNextPacket()
{
std::optional<PacketRef> ret;
if (m_position >= m_data.size())
return ret;
size_t new_position = m_position;
PacketHeader hdr;
std::memcpy(&hdr, &m_data[new_position], sizeof(hdr));
new_position += sizeof(hdr);
if ((new_position + (hdr.length * sizeof(u32))) > m_data.size())
return ret;
ret = PacketRef{.type = hdr.type,
.data = (hdr.length > 0) ?
std::span<const u32>(reinterpret_cast<const u32*>(&m_data[new_position]), hdr.length) :
std::span<const u32>()};
new_position += (hdr.length * sizeof(u32));
m_position = new_position;
return ret;
}
std::string_view GPUDump::Player::PacketRef::GetNullTerminatedString() const
{
return data.empty() ?
std::string_view() :
std::string_view(reinterpret_cast<const char*>(data.data()),
StringUtil::Strnlen(reinterpret_cast<const char*>(data.data()), data.size_bytes()));
}
bool GPUDump::Player::Preprocess(Error* error)
{
if (!ProcessHeader(error))
{
Error::AddPrefix(error, "Failed to process header: ");
return false;
}
m_position = m_start_offset;
if (!FindFrameStarts(error))
{
Error::AddPrefix(error, "Failed to process header: ");
return false;
}
m_position = m_start_offset;
return true;
}
bool GPUDump::Player::ProcessHeader(Error* error)
{
if (m_data.size() < sizeof(FILE_HEADER) || std::memcmp(m_data.data(), FILE_HEADER, sizeof(FILE_HEADER)) != 0)
{
Error::SetStringView(error, "File does not have the correct header.");
return false;
}
m_start_offset = sizeof(FILE_HEADER);
m_position = m_start_offset;
for (;;)
{
const std::optional<PacketRef> packet = GetNextPacket();
if (!packet.has_value())
{
Error::SetStringView(error, "EOF reached before reaching trace begin.");
return false;
}
switch (packet->type)
{
case PacketType::TextualVideoFormat:
{
const std::string_view region_str = packet->GetNullTerminatedString();
DEV_LOG("Dump video format: {}", region_str);
if (StringUtil::EqualNoCase(region_str, "NTSC"))
m_region = ConsoleRegion::NTSC_U;
else if (StringUtil::EqualNoCase(region_str, "PAL"))
m_region = ConsoleRegion::PAL;
else
WARNING_LOG("Unknown console region: {}", region_str);
}
break;
case PacketType::GameID:
{
const std::string_view serial = packet->GetNullTerminatedString();
DEV_LOG("Dump serial: {}", serial);
m_serial = serial;
}
break;
case PacketType::Comment:
{
const std::string_view comment = packet->GetNullTerminatedString();
DEV_LOG("Dump comment: {}", comment);
}
break;
case PacketType::TraceBegin:
{
DEV_LOG("Trace start found at offset {}", m_position);
return true;
}
default:
{
// ignore packet
}
break;
}
}
}
bool GPUDump::Player::FindFrameStarts(Error* error)
{
for (;;)
{
const std::optional<PacketRef> packet = GetNextPacket();
if (!packet.has_value())
break;
switch (packet->type)
{
case PacketType::TraceBegin:
{
if (!m_frame_offsets.empty())
{
Error::SetStringView(error, "VSync or trace begin event found before final trace begin.");
return false;
}
m_frame_offsets.push_back(m_position);
}
break;
case PacketType::VSyncEvent:
{
if (m_frame_offsets.empty())
{
Error::SetStringView(error, "Trace begin event missing before first VSync.");
return false;
}
m_frame_offsets.push_back(m_position);
}
break;
default:
{
// ignore packet
}
break;
}
}
if (m_frame_offsets.size() < 2)
{
Error::SetStringView(error, "Dump does not contain at least one frame.");
return false;
}
#ifdef _DEBUG
for (size_t i = 0; i < m_frame_offsets.size(); i++)
DEBUG_LOG("Frame {} starts at offset {}", i, m_frame_offsets[i]);
#endif
return true;
}
void GPUDump::Player::ProcessPacket(const PacketRef& pkt)
{
if (pkt.type <= PacketType::VSyncEvent)
{
// gp0/gp1/vsync => direct to gpu
g_gpu->ProcessGPUDumpPacket(pkt.type, pkt.data);
return;
}
}
void GPUDump::Player::Execute()
{
if (fastjmp_set(CPU::GetExecutionJmpBuf()) != 0)
return;
for (;;)
{
const std::optional<PacketRef> packet = GetNextPacket();
if (!packet.has_value())
{
m_position = g_settings.gpu_dump_fast_replay_mode ? m_frame_offsets.front() : m_start_offset;
continue;
}
ProcessPacket(packet.value());
}
}

143
src/core/gpu_dump.h Normal file
View File

@ -0,0 +1,143 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "gpu_types.h"
#include "common/bitfield.h"
#include "common/file_system.h"
#include <memory>
// Implements the specification from https://github.com/ps1dev/standards/blob/main/GPUDUMP.md
class Error;
namespace GPUDump {
enum class GPUVersion : u8
{
V1_1MB_VRAM,
V2_1MB_VRAM,
V2_2MB_VRAM,
};
enum class PacketType : u8
{
GPUPort0Data = 0x00,
GPUPort1Data = 0x01,
VSyncEvent = 0x02,
DiscardPort0Data = 0x03,
ReadbackPort0Data = 0x04,
TraceBegin = 0x05,
GPUVersion = 0x06,
GameID = 0x10,
TextualVideoFormat = 0x11,
Comment = 0x12,
};
static constexpr u32 MAX_PACKET_LENGTH = ((1u << 24) - 1); // 3 bytes for packet size
union PacketHeader
{
// Length0,Length1,Length2,Type
BitField<u32, u32, 0, 24> length;
BitField<u32, PacketType, 24, 8> type;
u32 bits;
};
static_assert(sizeof(PacketHeader) == 4);
class Recorder
{
public:
~Recorder();
static std::unique_ptr<Recorder> Create(std::string path, std::string_view serial, u32 num_frames, Error* error);
/// Compresses an already-created dump.
static bool Compress(const std::string& source_path, GPUDumpCompressionMode mode, Error* error);
ALWAYS_INLINE const std::string& GetPath() const { return m_path; }
/// Returns true if the caller should stop recording data.
bool IsFinished();
bool Close(Error* error);
void BeginPacket(PacketType packet, u32 minimum_size = 0);
ALWAYS_INLINE void WriteWord(u32 word) { m_packet_buffer.push_back(word); }
void WriteWords(const u32* words, size_t word_count);
void WriteWords(const std::span<const u32> words);
void WriteString(std::string_view str);
void WriteBytes(const void* data, size_t data_size_in_bytes);
void EndPacket();
void WriteGP1Command(GP1Command command, u32 param);
void BeginGP0Packet(u32 size);
void WriteGP0Packet(u32 word);
void EndGP0Packet();
void WriteGP1Packet(u32 value);
void WriteDiscardVRAMRead(u32 width, u32 height);
void WriteVSync(u64 ticks);
private:
Recorder(FileSystem::AtomicRenamedFile fp, u32 vsyncs_remaining, std::string path);
void WriteHeaders(std::string_view serial);
void WriteCurrentVRAM();
FileSystem::AtomicRenamedFile m_fp;
std::vector<u32> m_packet_buffer;
u32 m_vsyncs_remaining = 0;
PacketType m_current_packet = PacketType::Comment;
bool m_write_error = false;
std::string m_path;
};
class Player
{
public:
~Player();
ALWAYS_INLINE const std::string& GetPath() const { return m_path; }
ALWAYS_INLINE const std::string& GetSerial() const { return m_serial; }
ALWAYS_INLINE ConsoleRegion GetRegion() const { return m_region; }
static std::unique_ptr<Player> Open(std::string path, Error* error);
void Execute();
private:
Player(std::string path, DynamicHeapArray<u8> data);
struct PacketRef
{
PacketType type;
std::span<const u32> data;
std::string_view GetNullTerminatedString() const;
};
std::optional<PacketRef> GetNextPacket();
bool Preprocess(Error* error);
bool ProcessHeader(Error* error);
bool FindFrameStarts(Error* error);
void ProcessPacket(const PacketRef& pkt);
DynamicHeapArray<u8> m_data;
size_t m_start_offset = 0;
size_t m_position = 0;
std::string m_path;
std::string m_serial;
ConsoleRegion m_region = ConsoleRegion::NTSC_U;
std::vector<size_t> m_frame_offsets;
};
} // namespace GPUDump

View File

@ -44,6 +44,14 @@ enum : s32
MAX_PRIMITIVE_HEIGHT = 512, MAX_PRIMITIVE_HEIGHT = 512,
}; };
enum class GPUDMADirection : u8
{
Off = 0,
FIFO = 1,
CPUtoGP0 = 2,
GPUREADtoCPU = 3
};
enum class GPUPrimitive : u8 enum class GPUPrimitive : u8
{ {
Reserved = 0, Reserved = 0,
@ -92,6 +100,20 @@ enum class GPUInterlacedDisplayMode : u8
SeparateFields SeparateFields
}; };
enum class GP1Command : u8
{
ResetGPU = 0x00,
ClearFIFO = 0x01,
AcknowledgeInterrupt = 0x02,
SetDisplayDisable = 0x03,
SetDMADirection = 0x04,
SetDisplayStartAddress = 0x05,
SetHorizontalDisplayRange = 0x06,
SetVerticalDisplayRange = 0x07,
SetDisplayMode = 0x08,
SetAllowTextureDisable = 0x09,
};
// NOTE: Inclusive, not exclusive on the upper bounds. // NOTE: Inclusive, not exclusive on the upper bounds.
struct GPUDrawingArea struct GPUDrawingArea
{ {
@ -142,6 +164,68 @@ union GPURenderCommand
} }
}; };
union GP1SetDisplayMode
{
u32 bits;
BitField<u32, u8, 0, 2> horizontal_resolution_1;
BitField<u32, bool, 2, 1> vertical_resolution;
BitField<u32, bool, 3, 1> pal_mode;
BitField<u32, bool, 4, 1> display_area_color_depth;
BitField<u32, bool, 5, 1> vertical_interlace;
BitField<u32, bool, 6, 1> horizontal_resolution_2;
BitField<u32, bool, 7, 1> reverse_flag;
};
union GPUSTAT
{
// During transfer/render operations, if ((dst_pixel & mask_and) == 0) { pixel = src_pixel | mask_or }
u32 bits;
BitField<u32, u8, 0, 4> texture_page_x_base;
BitField<u32, u8, 4, 1> texture_page_y_base;
BitField<u32, GPUTransparencyMode, 5, 2> semi_transparency_mode;
BitField<u32, GPUTextureMode, 7, 2> texture_color_mode;
BitField<u32, bool, 9, 1> dither_enable;
BitField<u32, bool, 10, 1> draw_to_displayed_field;
BitField<u32, bool, 11, 1> set_mask_while_drawing;
BitField<u32, bool, 12, 1> check_mask_before_draw;
BitField<u32, u8, 13, 1> interlaced_field;
BitField<u32, bool, 14, 1> reverse_flag;
BitField<u32, bool, 15, 1> texture_disable;
BitField<u32, u8, 16, 1> horizontal_resolution_2;
BitField<u32, u8, 17, 2> horizontal_resolution_1;
BitField<u32, bool, 19, 1> vertical_resolution;
BitField<u32, bool, 20, 1> pal_mode;
BitField<u32, bool, 21, 1> display_area_color_depth_24;
BitField<u32, bool, 22, 1> vertical_interlace;
BitField<u32, bool, 23, 1> display_disable;
BitField<u32, bool, 24, 1> interrupt_request;
BitField<u32, bool, 25, 1> dma_data_request;
BitField<u32, bool, 26, 1> gpu_idle;
BitField<u32, bool, 27, 1> ready_to_send_vram;
BitField<u32, bool, 28, 1> ready_to_recieve_dma;
BitField<u32, GPUDMADirection, 29, 2> dma_direction;
BitField<u32, bool, 31, 1> display_line_lsb;
ALWAYS_INLINE bool IsMaskingEnabled() const
{
static constexpr u32 MASK = ((1 << 11) | (1 << 12));
return ((bits & MASK) != 0);
}
ALWAYS_INLINE bool SkipDrawingToActiveField() const
{
static constexpr u32 MASK = (1 << 19) | (1 << 22) | (1 << 10);
static constexpr u32 ACTIVE = (1 << 19) | (1 << 22);
return ((bits & MASK) == ACTIVE);
}
ALWAYS_INLINE bool InInterleaved480iMode() const
{
static constexpr u32 ACTIVE = (1 << 19) | (1 << 22);
return ((bits & ACTIVE) == ACTIVE);
}
};
ALWAYS_INLINE static constexpr u32 VRAMRGBA5551ToRGBA8888(u32 color) ALWAYS_INLINE static constexpr u32 VRAMRGBA5551ToRGBA8888(u32 color)
{ {
// Helper/format conversion functions - constants from https://stackoverflow.com/a/9069480 // Helper/format conversion functions - constants from https://stackoverflow.com/a/9069480

View File

@ -224,6 +224,20 @@ DEFINE_HOTKEY("Screenshot", TRANSLATE_NOOP("Hotkeys", "General"), TRANSLATE_NOOP
System::SaveScreenshot(); System::SaveScreenshot();
}) })
DEFINE_HOTKEY("RecordSingleFrameGPUDump", TRANSLATE_NOOP("Hotkeys", "Graphics"),
TRANSLATE_NOOP("Hotkeys", "Record Single Frame GPU Trace"), [](s32 pressed) {
if (!pressed)
System::StartRecordingGPUDump(nullptr, 1);
})
DEFINE_HOTKEY("RecordMultiFrameGPUDump", TRANSLATE_NOOP("Hotkeys", "Graphics"),
TRANSLATE_NOOP("Hotkeys", "Record Multi-Frame GPU Trace"), [](s32 pressed) {
if (pressed > 0)
System::StartRecordingGPUDump(nullptr, 0);
else
System::StopRecordingGPUDump();
})
#ifndef __ANDROID__ #ifndef __ANDROID__
DEFINE_HOTKEY("ToggleMediaCapture", TRANSLATE_NOOP("Hotkeys", "General"), DEFINE_HOTKEY("ToggleMediaCapture", TRANSLATE_NOOP("Hotkeys", "General"),
TRANSLATE_NOOP("Hotkeys", "Toggle Media Capture"), [](s32 pressed) { TRANSLATE_NOOP("Hotkeys", "Toggle Media Capture"), [](s32 pressed) {

View File

@ -248,6 +248,7 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si)
gpu_pgxp_depth_buffer = si.GetBoolValue("GPU", "PGXPDepthBuffer", false); gpu_pgxp_depth_buffer = si.GetBoolValue("GPU", "PGXPDepthBuffer", false);
gpu_pgxp_disable_2d = si.GetBoolValue("GPU", "PGXPDisableOn2DPolygons", false); gpu_pgxp_disable_2d = si.GetBoolValue("GPU", "PGXPDisableOn2DPolygons", false);
SetPGXPDepthClearThreshold(si.GetFloatValue("GPU", "PGXPDepthClearThreshold", DEFAULT_GPU_PGXP_DEPTH_THRESHOLD)); SetPGXPDepthClearThreshold(si.GetFloatValue("GPU", "PGXPDepthClearThreshold", DEFAULT_GPU_PGXP_DEPTH_THRESHOLD));
gpu_dump_fast_replay_mode = si.GetBoolValue("GPU", "DumpFastReplayMode", false);
display_deinterlacing_mode = display_deinterlacing_mode =
ParseDisplayDeinterlacingMode( ParseDisplayDeinterlacingMode(
@ -570,6 +571,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetBoolValue("GPU", "PGXPDepthBuffer", gpu_pgxp_depth_buffer); si.SetBoolValue("GPU", "PGXPDepthBuffer", gpu_pgxp_depth_buffer);
si.SetBoolValue("GPU", "PGXPDisableOn2DPolygons", gpu_pgxp_disable_2d); si.SetBoolValue("GPU", "PGXPDisableOn2DPolygons", gpu_pgxp_disable_2d);
si.SetFloatValue("GPU", "PGXPDepthClearThreshold", GetPGXPDepthClearThreshold()); si.SetFloatValue("GPU", "PGXPDepthClearThreshold", GetPGXPDepthClearThreshold());
si.SetBoolValue("GPU", "DumpFastReplayMode", gpu_dump_fast_replay_mode);
si.SetStringValue("GPU", "DeinterlacingMode", GetDisplayDeinterlacingModeName(display_deinterlacing_mode)); si.SetStringValue("GPU", "DeinterlacingMode", GetDisplayDeinterlacingModeName(display_deinterlacing_mode));
si.SetStringValue("Display", "CropMode", GetDisplayCropModeName(display_crop_mode)); si.SetStringValue("Display", "CropMode", GetDisplayCropModeName(display_crop_mode));
@ -1519,6 +1521,42 @@ const char* Settings::GetGPUWireframeModeDisplayName(GPUWireframeMode mode)
"GPUWireframeMode"); "GPUWireframeMode");
} }
static constexpr const std::array s_gpu_dump_compression_mode_names = {"Disabled", "ZstLow", "ZstDefault", "ZstHigh"};
static constexpr const std::array s_gpu_dump_compression_mode_display_names = {
TRANSLATE_DISAMBIG_NOOP("Settings", "Disabled", "GPUDumpCompressionMode"),
TRANSLATE_DISAMBIG_NOOP("Settings", "Zstandard (Low)", "GPUDumpCompressionMode"),
TRANSLATE_DISAMBIG_NOOP("Settings", "Zstandard (Default)", "GPUDumpCompressionMode"),
TRANSLATE_DISAMBIG_NOOP("Settings", "Zstandard (High)", "GPUDumpCompressionMode"),
};
static_assert(s_gpu_dump_compression_mode_names.size() == static_cast<size_t>(GPUDumpCompressionMode::MaxCount));
static_assert(s_gpu_dump_compression_mode_display_names.size() ==
static_cast<size_t>(GPUDumpCompressionMode::MaxCount));
std::optional<GPUDumpCompressionMode> Settings::ParseGPUDumpCompressionMode(const char* str)
{
int index = 0;
for (const char* name : s_gpu_dump_compression_mode_names)
{
if (StringUtil::Strcasecmp(name, str) == 0)
return static_cast<GPUDumpCompressionMode>(index);
index++;
}
return std::nullopt;
}
const char* Settings::GetGPUDumpCompressionModeName(GPUDumpCompressionMode mode)
{
return s_gpu_dump_compression_mode_names[static_cast<size_t>(mode)];
}
const char* Settings::GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMode mode)
{
return Host::TranslateToCString("Settings", s_gpu_dump_compression_mode_display_names[static_cast<size_t>(mode)],
"GPUDumpCompressionMode");
}
static constexpr const std::array s_display_deinterlacing_mode_names = { static constexpr const std::array s_display_deinterlacing_mode_names = {
"Disabled", "Weave", "Blend", "Adaptive", "Progressive", "Disabled", "Weave", "Blend", "Adaptive", "Progressive",
}; };

View File

@ -279,6 +279,7 @@ struct Settings
bool bios_patch_fast_boot : 1 = DEFAULT_FAST_BOOT_VALUE; bool bios_patch_fast_boot : 1 = DEFAULT_FAST_BOOT_VALUE;
bool bios_fast_forward_boot : 1 = false; bool bios_fast_forward_boot : 1 = false;
bool enable_8mb_ram : 1 = false; bool enable_8mb_ram : 1 = false;
bool gpu_dump_fast_replay_mode : 1 = false;
std::array<ControllerType, NUM_CONTROLLER_AND_CARD_PORTS> controller_types{}; std::array<ControllerType, NUM_CONTROLLER_AND_CARD_PORTS> controller_types{};
std::array<MemoryCardType, NUM_CONTROLLER_AND_CARD_PORTS> memory_card_types{}; std::array<MemoryCardType, NUM_CONTROLLER_AND_CARD_PORTS> memory_card_types{};
@ -423,6 +424,10 @@ struct Settings
static const char* GetGPUWireframeModeName(GPUWireframeMode mode); static const char* GetGPUWireframeModeName(GPUWireframeMode mode);
static const char* GetGPUWireframeModeDisplayName(GPUWireframeMode mode); static const char* GetGPUWireframeModeDisplayName(GPUWireframeMode mode);
static std::optional<GPUDumpCompressionMode> ParseGPUDumpCompressionMode(const char* str);
static const char* GetGPUDumpCompressionModeName(GPUDumpCompressionMode mode);
static const char* GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMode mode);
static std::optional<DisplayDeinterlacingMode> ParseDisplayDeinterlacingMode(const char* str); static std::optional<DisplayDeinterlacingMode> ParseDisplayDeinterlacingMode(const char* str);
static const char* GetDisplayDeinterlacingModeName(DisplayDeinterlacingMode mode); static const char* GetDisplayDeinterlacingModeName(DisplayDeinterlacingMode mode);
static const char* GetDisplayDeinterlacingModeDisplayName(DisplayDeinterlacingMode mode); static const char* GetDisplayDeinterlacingModeDisplayName(DisplayDeinterlacingMode mode);
@ -485,6 +490,7 @@ struct Settings
static constexpr GPULineDetectMode DEFAULT_GPU_LINE_DETECT_MODE = GPULineDetectMode::Disabled; static constexpr GPULineDetectMode DEFAULT_GPU_LINE_DETECT_MODE = GPULineDetectMode::Disabled;
static constexpr GPUDownsampleMode DEFAULT_GPU_DOWNSAMPLE_MODE = GPUDownsampleMode::Disabled; static constexpr GPUDownsampleMode DEFAULT_GPU_DOWNSAMPLE_MODE = GPUDownsampleMode::Disabled;
static constexpr GPUWireframeMode DEFAULT_GPU_WIREFRAME_MODE = GPUWireframeMode::Disabled; static constexpr GPUWireframeMode DEFAULT_GPU_WIREFRAME_MODE = GPUWireframeMode::Disabled;
static constexpr GPUDumpCompressionMode DEFAULT_GPU_DUMP_COMPRESSION_MODE = GPUDumpCompressionMode::ZstDefault;
static constexpr ConsoleRegion DEFAULT_CONSOLE_REGION = ConsoleRegion::Auto; static constexpr ConsoleRegion DEFAULT_CONSOLE_REGION = ConsoleRegion::Auto;
static constexpr float DEFAULT_GPU_PGXP_DEPTH_THRESHOLD = 300.0f; static constexpr float DEFAULT_GPU_PGXP_DEPTH_THRESHOLD = 300.0f;
static constexpr float GPU_PGXP_DEPTH_THRESHOLD_SCALE = 4096.0f; static constexpr float GPU_PGXP_DEPTH_THRESHOLD_SCALE = 4096.0f;

View File

@ -16,6 +16,7 @@
#include "game_database.h" #include "game_database.h"
#include "game_list.h" #include "game_list.h"
#include "gpu.h" #include "gpu.h"
#include "gpu_dump.h"
#include "gpu_hw_texture_cache.h" #include "gpu_hw_texture_cache.h"
#include "gte.h" #include "gte.h"
#include "host.h" #include "host.h"
@ -167,6 +168,7 @@ static bool CreateGPU(GPURenderer renderer, bool is_switching, bool fullscreen,
static bool RecreateGPU(GPURenderer renderer, bool force_recreate_device = false, bool update_display = true); static bool RecreateGPU(GPURenderer renderer, bool force_recreate_device = false, bool update_display = true);
static void HandleHostGPUDeviceLost(); static void HandleHostGPUDeviceLost();
static void HandleExclusiveFullscreenLost(); static void HandleExclusiveFullscreenLost();
static std::string GetScreenshotPath(const char* extension);
/// Returns true if boot is being fast forwarded. /// Returns true if boot is being fast forwarded.
static bool IsFastForwardingBoot(); static bool IsFastForwardingBoot();
@ -224,6 +226,9 @@ static void DoRewind();
static void SaveRunaheadState(); static void SaveRunaheadState();
static bool DoRunahead(); static bool DoRunahead();
static bool OpenGPUDump(std::string path, Error* error);
static bool ChangeGPUDump(std::string new_path);
static void UpdateSessionTime(const std::string& prev_serial); static void UpdateSessionTime(const std::string& prev_serial);
#ifdef ENABLE_DISCORD_PRESENCE #ifdef ENABLE_DISCORD_PRESENCE
@ -320,6 +325,7 @@ static Common::Timer s_frame_timer;
static Threading::ThreadHandle s_cpu_thread_handle; static Threading::ThreadHandle s_cpu_thread_handle;
static std::unique_ptr<MediaCapture> s_media_capture; static std::unique_ptr<MediaCapture> s_media_capture;
static std::unique_ptr<GPUDump::Player> s_gpu_dump_player;
// temporary save state, created when loading, used to undo load state // temporary save state, created when loading, used to undo load state
static std::optional<System::SaveStateBuffer> s_undo_load_state; static std::optional<System::SaveStateBuffer> s_undo_load_state;
@ -631,6 +637,11 @@ bool System::IsExecuting()
return s_system_executing; return s_system_executing;
} }
bool System::IsReplayingGPUDump()
{
return static_cast<bool>(s_gpu_dump_player);
}
bool System::IsStartupCancelled() bool System::IsStartupCancelled()
{ {
return s_startup_cancelled.load(); return s_startup_cancelled.load();
@ -845,23 +856,29 @@ u32 System::GetFrameTimeHistoryPos()
return s_frame_time_history_pos; return s_frame_time_history_pos;
} }
bool System::IsExeFileName(std::string_view path) bool System::IsExePath(std::string_view path)
{ {
return (StringUtil::EndsWithNoCase(path, ".exe") || StringUtil::EndsWithNoCase(path, ".psexe") || return (StringUtil::EndsWithNoCase(path, ".exe") || StringUtil::EndsWithNoCase(path, ".psexe") ||
StringUtil::EndsWithNoCase(path, ".ps-exe") || StringUtil::EndsWithNoCase(path, ".psx")); StringUtil::EndsWithNoCase(path, ".ps-exe") || StringUtil::EndsWithNoCase(path, ".psx"));
} }
bool System::IsPsfFileName(std::string_view path) bool System::IsPsfPath(std::string_view path)
{ {
return (StringUtil::EndsWithNoCase(path, ".psf") || StringUtil::EndsWithNoCase(path, ".minipsf")); return (StringUtil::EndsWithNoCase(path, ".psf") || StringUtil::EndsWithNoCase(path, ".minipsf"));
} }
bool System::IsLoadableFilename(std::string_view path) bool System::IsGPUDumpPath(std::string_view path)
{
return (StringUtil::EndsWithNoCase(path, ".psxgpu") || StringUtil::EndsWithNoCase(path, ".psxgpu.zst"));
}
bool System::IsLoadablePath(std::string_view path)
{ {
static constexpr const std::array extensions = { static constexpr const std::array extensions = {
".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs ".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs
".exe", ".psexe", ".ps-exe", ".psx", // exes ".exe", ".psexe", ".ps-exe", ".psx", // exes
".psf", ".minipsf", // psf ".psf", ".minipsf", // psf
".psxgpu", ".psxgpu.zst", // gpu dump
".m3u", // playlists ".m3u", // playlists
".pbp", ".pbp",
}; };
@ -875,7 +892,7 @@ bool System::IsLoadableFilename(std::string_view path)
return false; return false;
} }
bool System::IsSaveStateFilename(std::string_view path) bool System::IsSaveStatePath(std::string_view path)
{ {
return StringUtil::EndsWithNoCase(path, ".sav"); return StringUtil::EndsWithNoCase(path, ".sav");
} }
@ -1556,6 +1573,7 @@ bool System::UpdateGameSettingsLayer()
s_input_settings_interface = std::move(input_interface); s_input_settings_interface = std::move(input_interface);
s_input_profile_name = std::move(input_profile_name); s_input_profile_name = std::move(input_profile_name);
if (!IsReplayingGPUDump())
Cheats::ReloadCheats(false, true, false, true); Cheats::ReloadCheats(false, true, false, true);
return true; return true;
@ -1693,16 +1711,29 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
std::string exe_override; std::string exe_override;
if (!parameters.filename.empty()) if (!parameters.filename.empty())
{ {
if (IsExeFileName(parameters.filename)) if (IsExePath(parameters.filename))
{ {
boot_mode = BootMode::BootEXE; boot_mode = BootMode::BootEXE;
exe_override = parameters.filename; exe_override = parameters.filename;
} }
else if (IsPsfFileName(parameters.filename)) else if (IsPsfPath(parameters.filename))
{ {
boot_mode = BootMode::BootPSF; boot_mode = BootMode::BootPSF;
exe_override = parameters.filename; exe_override = parameters.filename;
} }
else if (IsGPUDumpPath(parameters.filename))
{
if (!OpenGPUDump(parameters.filename, error))
{
s_state = State::Shutdown;
Host::OnSystemDestroyed();
Host::OnIdleStateChanged();
return false;
}
boot_mode = BootMode::ReplayGPUDump;
}
if (boot_mode == BootMode::BootEXE || boot_mode == BootMode::BootPSF) if (boot_mode == BootMode::BootEXE || boot_mode == BootMode::BootPSF)
{ {
if (s_region == ConsoleRegion::Auto) if (s_region == ConsoleRegion::Auto)
@ -1714,7 +1745,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
s_region = GetConsoleRegionForDiscRegion(file_region); s_region = GetConsoleRegionForDiscRegion(file_region);
} }
} }
else else if (boot_mode != BootMode::ReplayGPUDump)
{ {
INFO_LOG("Loading CD image '{}'...", Path::GetFileName(parameters.filename)); INFO_LOG("Loading CD image '{}'...", Path::GetFileName(parameters.filename));
disc = CDImage::Open(parameters.filename.c_str(), g_settings.cdrom_load_image_patches, error); disc = CDImage::Open(parameters.filename.c_str(), g_settings.cdrom_load_image_patches, error);
@ -1770,6 +1801,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
Error::AddPrefixFmt(error, "Failed to switch to subimage {} in '{}':\n", parameters.media_playlist_index, Error::AddPrefixFmt(error, "Failed to switch to subimage {} in '{}':\n", parameters.media_playlist_index,
Path::GetFileName(parameters.filename)); Path::GetFileName(parameters.filename));
s_state = State::Shutdown; s_state = State::Shutdown;
s_gpu_dump_player.reset();
Host::OnSystemDestroyed(); Host::OnSystemDestroyed();
Host::OnIdleStateChanged(); Host::OnIdleStateChanged();
return false; return false;
@ -1781,11 +1813,12 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
// Get boot EXE override. // Get boot EXE override.
if (!parameters.override_exe.empty()) if (!parameters.override_exe.empty())
{ {
if (!FileSystem::FileExists(parameters.override_exe.c_str()) || !IsExeFileName(parameters.override_exe)) if (!FileSystem::FileExists(parameters.override_exe.c_str()) || !IsExePath(parameters.override_exe))
{ {
Error::SetStringFmt(error, "File '{}' is not a valid executable to boot.", Error::SetStringFmt(error, "File '{}' is not a valid executable to boot.",
Path::GetFileName(parameters.override_exe)); Path::GetFileName(parameters.override_exe));
s_state = State::Shutdown; s_state = State::Shutdown;
s_gpu_dump_player.reset();
Cheats::UnloadAll(); Cheats::UnloadAll();
ClearRunningGame(); ClearRunningGame();
Host::OnSystemDestroyed(); Host::OnSystemDestroyed();
@ -1802,6 +1835,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (!CheckForSBIFile(disc.get(), error)) if (!CheckForSBIFile(disc.get(), error))
{ {
s_state = State::Shutdown; s_state = State::Shutdown;
s_gpu_dump_player.reset();
Cheats::UnloadAll(); Cheats::UnloadAll();
ClearRunningGame(); ClearRunningGame();
Host::OnSystemDestroyed(); Host::OnSystemDestroyed();
@ -1840,6 +1874,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (cancelled) if (cancelled)
{ {
s_state = State::Shutdown; s_state = State::Shutdown;
s_gpu_dump_player.reset();
Cheats::UnloadAll(); Cheats::UnloadAll();
ClearRunningGame(); ClearRunningGame();
Host::OnSystemDestroyed(); Host::OnSystemDestroyed();
@ -1854,6 +1889,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (!SetBootMode(boot_mode, disc_region, error)) if (!SetBootMode(boot_mode, disc_region, error))
{ {
s_state = State::Shutdown; s_state = State::Shutdown;
s_gpu_dump_player.reset();
Cheats::UnloadAll(); Cheats::UnloadAll();
ClearRunningGame(); ClearRunningGame();
Host::OnSystemDestroyed(); Host::OnSystemDestroyed();
@ -1867,6 +1903,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
{ {
s_boot_mode = System::BootMode::None; s_boot_mode = System::BootMode::None;
s_state = State::Shutdown; s_state = State::Shutdown;
s_gpu_dump_player.reset();
Cheats::UnloadAll(); Cheats::UnloadAll();
ClearRunningGame(); ClearRunningGame();
Host::OnSystemDestroyed(); Host::OnSystemDestroyed();
@ -2038,6 +2075,8 @@ void System::DestroySystem()
ImGuiManager::DestroyAllDebugWindows(); ImGuiManager::DestroyAllDebugWindows();
s_gpu_dump_player.reset();
s_undo_load_state.reset(); s_undo_load_state.reset();
#ifdef ENABLE_GDB_SERVER #ifdef ENABLE_GDB_SERVER
@ -2133,7 +2172,9 @@ void System::Execute()
g_gpu->RestoreDeviceContext(); g_gpu->RestoreDeviceContext();
TimingEvents::CommitLeftoverTicks(); TimingEvents::CommitLeftoverTicks();
if (s_rewind_load_counter >= 0) if (s_gpu_dump_player) [[unlikely]]
s_gpu_dump_player->Execute();
else if (s_rewind_load_counter >= 0)
DoRewind(); DoRewind();
else else
CPU::Execute(); CPU::Execute();
@ -2164,12 +2205,15 @@ void System::FrameDone()
// Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns. // Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns.
// TODO: when running ahead, we can skip this (and the flush above) // TODO: when running ahead, we can skip this (and the flush above)
if (!IsReplayingGPUDump()) [[likely]]
{
SPU::GeneratePendingSamples(); SPU::GeneratePendingSamples();
Cheats::ApplyFrameEndCodes(); Cheats::ApplyFrameEndCodes();
if (Achievements::IsActive()) if (Achievements::IsActive())
Achievements::FrameUpdate(); Achievements::FrameUpdate();
}
#ifdef ENABLE_DISCORD_PRESENCE #ifdef ENABLE_DISCORD_PRESENCE
PollDiscordPresence(); PollDiscordPresence();
@ -2697,7 +2741,7 @@ bool System::SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, Error*
return true; return true;
// Need to reload the BIOS to wipe out the patching. // Need to reload the BIOS to wipe out the patching.
if (!LoadBIOS(error)) if (new_boot_mode != BootMode::ReplayGPUDump && !LoadBIOS(error))
return false; return false;
// Handle the case of BIOSes not being able to full boot. // Handle the case of BIOSes not being able to full boot.
@ -2745,9 +2789,9 @@ std::string System::GetMediaPathFromSaveState(const char* path)
bool System::LoadState(const char* path, Error* error, bool save_undo_state) bool System::LoadState(const char* path, Error* error, bool save_undo_state)
{ {
if (!IsValid()) if (!IsValid() || IsReplayingGPUDump())
{ {
Error::SetStringView(error, "System is not booted."); Error::SetStringView(error, TRANSLATE_SV("System", "System is not in correct state."));
return false; return false;
} }
@ -3067,7 +3111,12 @@ bool System::ReadAndDecompressStateData(std::FILE* fp, std::span<u8> dst, u32 fi
bool System::SaveState(const char* path, Error* error, bool backup_existing_save) bool System::SaveState(const char* path, Error* error, bool backup_existing_save)
{ {
if (IsSavingMemoryCards()) if (!IsValid() || IsReplayingGPUDump())
{
Error::SetStringView(error, TRANSLATE_SV("System", "System is not in correct state."));
return false;
}
else if (IsSavingMemoryCards())
{ {
Error::SetStringView(error, TRANSLATE_SV("System", "Cannot save state while memory card is being saved.")); Error::SetStringView(error, TRANSLATE_SV("System", "Cannot save state while memory card is being saved."));
return false; return false;
@ -3780,7 +3829,7 @@ void System::ResetControllers()
std::unique_ptr<MemoryCard> System::GetMemoryCardForSlot(u32 slot, MemoryCardType type) std::unique_ptr<MemoryCard> System::GetMemoryCardForSlot(u32 slot, MemoryCardType type)
{ {
// Disable memory cards when running PSFs. // Disable memory cards when running PSFs.
const bool is_running_psf = !s_running_game_path.empty() && IsPsfFileName(s_running_game_path.c_str()); const bool is_running_psf = !s_running_game_path.empty() && IsPsfPath(s_running_game_path.c_str());
if (is_running_psf) if (is_running_psf)
return nullptr; return nullptr;
@ -4070,6 +4119,9 @@ std::string System::GetMediaFileName()
bool System::InsertMedia(const char* path) bool System::InsertMedia(const char* path)
{ {
if (IsGPUDumpPath(path)) [[unlikely]]
return ChangeGPUDump(path);
Error error; Error error;
std::unique_ptr<CDImage> image = CDImage::Open(path, g_settings.cdrom_load_image_patches, &error); std::unique_ptr<CDImage> image = CDImage::Open(path, g_settings.cdrom_load_image_patches, &error);
if (!image) if (!image)
@ -4130,7 +4182,7 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
s_running_game_title = GameList::GetCustomTitleForPath(s_running_game_path); s_running_game_title = GameList::GetCustomTitleForPath(s_running_game_path);
s_running_game_custom_title = !s_running_game_title.empty(); s_running_game_custom_title = !s_running_game_title.empty();
if (IsExeFileName(path)) if (IsExePath(path))
{ {
if (s_running_game_title.empty()) if (s_running_game_title.empty())
s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path)); s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));
@ -4139,12 +4191,28 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
if (s_running_game_hash != 0) if (s_running_game_hash != 0)
s_running_game_serial = GetGameHashId(s_running_game_hash); s_running_game_serial = GetGameHashId(s_running_game_hash);
} }
else if (IsPsfFileName(path)) else if (IsPsfPath(path))
{ {
// TODO: We could pull the title from the PSF. // TODO: We could pull the title from the PSF.
if (s_running_game_title.empty()) if (s_running_game_title.empty())
s_running_game_title = Path::GetFileTitle(path); s_running_game_title = Path::GetFileTitle(path);
} }
else if (IsGPUDumpPath(path))
{
DebugAssert(s_gpu_dump_player);
if (s_gpu_dump_player)
{
s_running_game_serial = s_gpu_dump_player->GetSerial();
if (!s_running_game_serial.empty())
{
s_running_game_entry = GameDatabase::GetEntryForSerial(s_running_game_serial);
if (s_running_game_entry && s_running_game_title.empty())
s_running_game_title = s_running_game_entry->title;
else if (s_running_game_title.empty())
s_running_game_title = s_running_game_serial;
}
}
}
// Check for an audio CD. Those shouldn't set any title. // Check for an audio CD. Those shouldn't set any title.
else if (image && image->GetTrack(1).mode != CDImage::TrackMode::Audio) else if (image && image->GetTrack(1).mode != CDImage::TrackMode::Audio)
{ {
@ -4180,6 +4248,8 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
if (!booting) if (!booting)
GPUTextureCache::SetGameID(s_running_game_serial); GPUTextureCache::SetGameID(s_running_game_serial);
if (!IsReplayingGPUDump())
{
if (booting) if (booting)
Achievements::ResetHardcoreMode(true); Achievements::ResetHardcoreMode(true);
@ -4187,6 +4257,8 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
// game layer reloads cheats, but only the active list, we need new files // game layer reloads cheats, but only the active list, we need new files
Cheats::ReloadCheats(true, false, false, true); Cheats::ReloadCheats(true, false, false, true);
}
UpdateGameSettingsLayer(); UpdateGameSettingsLayer();
ApplySettings(true); ApplySettings(true);
@ -4879,6 +4951,14 @@ void System::UpdateMemorySaveStateSettings()
{ {
ClearMemorySaveStates(); ClearMemorySaveStates();
if (IsReplayingGPUDump()) [[unlikely]]
{
s_memory_saves_enabled = false;
s_rewind_save_counter = -1;
s_runahead_frames = 0;
return;
}
s_memory_saves_enabled = g_settings.rewind_enable; s_memory_saves_enabled = g_settings.rewind_enable;
if (g_settings.rewind_enable) if (g_settings.rewind_enable)
@ -5259,38 +5339,60 @@ void System::UpdateVolume()
SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume()); SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume());
} }
bool System::SaveScreenshot(const char* filename, DisplayScreenshotMode mode, DisplayScreenshotFormat format, std::string System::GetScreenshotPath(const char* extension)
u8 quality, bool compress_on_thread)
{
if (!System::IsValid())
return false;
std::string auto_filename;
if (!filename)
{ {
const std::string sanitized_name = Path::SanitizeFileName(System::GetGameTitle()); const std::string sanitized_name = Path::SanitizeFileName(System::GetGameTitle());
const char* extension = Settings::GetDisplayScreenshotFormatExtension(format);
std::string basename; std::string basename;
if (sanitized_name.empty()) if (sanitized_name.empty())
basename = fmt::format("{}", GetTimestampStringForFileName()); basename = fmt::format("{}", GetTimestampStringForFileName());
else else
basename = fmt::format("{} {}", sanitized_name, GetTimestampStringForFileName()); basename = fmt::format("{} {}", sanitized_name, GetTimestampStringForFileName());
auto_filename = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.{}", EmuFolders::Screenshots, basename, extension); std::string path = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.{}", EmuFolders::Screenshots, basename, extension);
// handle quick screenshots to the same filename // handle quick screenshots to the same filename
u32 next_suffix = 1; u32 next_suffix = 1;
while (FileSystem::FileExists(auto_filename.c_str())) while (FileSystem::FileExists(path.c_str()))
{ {
auto_filename = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{} ({}).{}", EmuFolders::Screenshots, basename, path =
next_suffix, extension); fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{} ({}).{}", EmuFolders::Screenshots, basename, next_suffix, extension);
next_suffix++; next_suffix++;
} }
filename = auto_filename.c_str(); return path;
} }
return g_gpu->RenderScreenshotToFile(filename, mode, quality, compress_on_thread, true); bool System::SaveScreenshot(const char* path, DisplayScreenshotMode mode, DisplayScreenshotFormat format, u8 quality,
bool compress_on_thread)
{
if (!IsValid())
return false;
std::string auto_path;
if (!path)
path = (auto_path = GetScreenshotPath(Settings::GetDisplayScreenshotFormatExtension(format))).c_str();
return g_gpu->RenderScreenshotToFile(path, mode, quality, compress_on_thread, true);
}
bool System::StartRecordingGPUDump(const char* path /*= nullptr*/, u32 num_frames /*= 0*/)
{
if (!IsValid() || IsReplayingGPUDump())
return false;
std::string auto_path;
if (!path)
path = (auto_path = GetScreenshotPath("psxgpu")).c_str();
return g_gpu->StartRecordingGPUDump(path, num_frames);
}
void System::StopRecordingGPUDump()
{
if (!IsValid())
return;
g_gpu->StopRecordingGPUDump();
} }
static std::string_view GetCaptureTypeForMessage(bool capture_video, bool capture_audio) static std::string_view GetCaptureTypeForMessage(bool capture_video, bool capture_audio)
@ -5833,6 +5935,34 @@ void System::InvalidateDisplay()
g_gpu->RestoreDeviceContext(); g_gpu->RestoreDeviceContext();
} }
bool System::OpenGPUDump(std::string path, Error* error)
{
std::unique_ptr<GPUDump::Player> new_dump = GPUDump::Player::Open(std::move(path), error);
if (!new_dump)
return false;
// set properties
s_gpu_dump_player = std::move(new_dump);
s_region = s_gpu_dump_player->GetRegion();
return true;
}
bool System::ChangeGPUDump(std::string new_path)
{
Error error;
if (!OpenGPUDump(std::move(new_path), &error))
{
Host::ReportErrorAsync("Error", fmt::format(TRANSLATE_FS("Failed to change GPU dump: {}", error.GetDescription())));
return false;
}
UpdateRunningGame(s_gpu_dump_player->GetPath(), nullptr, false);
// current player object has been changed out, toss call stack
InterruptExecution();
return true;
}
void System::UpdateSessionTime(const std::string& prev_serial) void System::UpdateSessionTime(const std::string& prev_serial)
{ {
const u64 ctime = Common::Timer::GetCurrentValue(); const u64 ctime = Common::Timer::GetCurrentValue();

View File

@ -102,6 +102,7 @@ enum class BootMode
FastBoot, FastBoot,
BootEXE, BootEXE,
BootPSF, BootPSF,
ReplayGPUDump,
}; };
enum class Taint : u8 enum class Taint : u8
@ -118,17 +119,20 @@ enum class Taint : u8
extern TickCount g_ticks_per_second; extern TickCount g_ticks_per_second;
/// Returns true if the filename is a PlayStation executable we can inject. /// Returns true if the path is a PlayStation executable we can inject.
bool IsExeFileName(std::string_view path); bool IsExePath(std::string_view path);
/// Returns true if the filename is a Portable Sound Format file we can uncompress/load. /// Returns true if the path is a Portable Sound Format file we can uncompress/load.
bool IsPsfFileName(std::string_view path); bool IsPsfPath(std::string_view path);
/// Returns true if the filename is one we can load. /// Returns true if the path is a GPU dump that we can replay.
bool IsLoadableFilename(std::string_view path); bool IsGPUDumpPath(std::string_view path);
/// Returns true if the filename is a save state. /// Returns true if the path is one we can load.
bool IsSaveStateFilename(std::string_view path); bool IsLoadablePath(std::string_view path);
/// Returns true if the path is a save state.
bool IsSaveStatePath(std::string_view path);
/// Returns the preferred console type for a disc. /// Returns the preferred console type for a disc.
ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region); ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region);
@ -159,6 +163,7 @@ bool IsShutdown();
bool IsValid(); bool IsValid();
bool IsValidOrInitializing(); bool IsValidOrInitializing();
bool IsExecuting(); bool IsExecuting();
bool IsReplayingGPUDump();
bool IsStartupCancelled(); bool IsStartupCancelled();
void CancelPendingStartup(); void CancelPendingStartup();
@ -390,10 +395,14 @@ s32 GetAudioOutputVolume();
void UpdateVolume(); void UpdateVolume();
/// Saves a screenshot to the specified file. If no file name is provided, one will be generated automatically. /// Saves a screenshot to the specified file. If no file name is provided, one will be generated automatically.
bool SaveScreenshot(const char* filename = nullptr, DisplayScreenshotMode mode = g_settings.display_screenshot_mode, bool SaveScreenshot(const char* path = nullptr, DisplayScreenshotMode mode = g_settings.display_screenshot_mode,
DisplayScreenshotFormat format = g_settings.display_screenshot_format, DisplayScreenshotFormat format = g_settings.display_screenshot_format,
u8 quality = g_settings.display_screenshot_quality, bool compress_on_thread = true); u8 quality = g_settings.display_screenshot_quality, bool compress_on_thread = true);
/// Starts/stops GPU dump/trace recording.
bool StartRecordingGPUDump(const char* path = nullptr, u32 num_frames = 1);
void StopRecordingGPUDump();
/// Returns the path that a new media capture would be saved to by default. Safe to call from any thread. /// Returns the path that a new media capture would be saved to by default. Safe to call from any thread.
std::string GetNewMediaCapturePath(const std::string_view title, const std::string_view container); std::string GetNewMediaCapturePath(const std::string_view title, const std::string_view container);

View File

@ -5,6 +5,7 @@
#include "gpu.h" #include "gpu.h"
#include "interrupt_controller.h" #include "interrupt_controller.h"
#include "system.h" #include "system.h"
#include "timing_event.h"
#include "util/imgui_manager.h" #include "util/imgui_manager.h"
#include "util/state_wrapper.h" #include "util/state_wrapper.h"

View File

@ -88,6 +88,11 @@ TimingEvent** TimingEvents::GetHeadEventPtr()
return &s_state.active_events_head; return &s_state.active_events_head;
} }
void TimingEvents::SetGlobalTickCounter(GlobalTicks ticks)
{
s_state.global_tick_counter = ticks;
}
void TimingEvents::SortEvent(TimingEvent* event) void TimingEvents::SortEvent(TimingEvent* event)
{ {
const GlobalTicks event_runtime = event->m_next_run_time; const GlobalTicks event_runtime = event->m_next_run_time;

View File

@ -94,4 +94,7 @@ void UpdateCPUDowncount();
TimingEvent** GetHeadEventPtr(); TimingEvent** GetHeadEventPtr();
// Tick counter injection, only for GPU dump replayer.
void SetGlobalTickCounter(GlobalTicks ticks);
} // namespace TimingEvents } // namespace TimingEvents

View File

@ -126,6 +126,16 @@ enum class GPULineDetectMode : u8
Count Count
}; };
enum class GPUDumpCompressionMode : u8
{
Disabled,
ZstLow,
ZstDefault,
ZstHigh,
// TODO: XZ
MaxCount
};
enum class DisplayCropMode : u8 enum class DisplayCropMode : u8
{ {
None, None,

View File

@ -294,6 +294,12 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.gpuThread, "GPU", "UseThread", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.gpuThread, "GPU", "UseThread", true);
SettingWidgetBinder::BindWidgetToEnumSetting(
sif, m_ui.gpuDumpCompressionMode, "GPU", "DumpCompressionMode", &Settings::ParseGPUDumpCompressionMode,
&Settings::GetGPUDumpCompressionModeName, &Settings::GetGPUDumpCompressionModeDisplayName,
Settings::DEFAULT_GPU_DUMP_COMPRESSION_MODE, GPUDumpCompressionMode::MaxCount);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.gpuDumpFastReplayMode, "GPU", "DumpFastReplayMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useDebugDevice, "GPU", "UseDebugDevice", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useDebugDevice, "GPU", "UseDebugDevice", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableShaderCache, "GPU", "DisableShaderCache", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableShaderCache, "GPU", "DisableShaderCache", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableDualSource, "GPU", "DisableDualSourceBlend", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableDualSource, "GPU", "DisableDualSourceBlend", false);

View File

@ -1297,6 +1297,32 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="gpuDumpGroup">
<property name="title">
<string>GPU Dump Recording/Playback</string>
</property>
<layout class="QFormLayout" name="formLayout_12">
<item row="0" column="0">
<widget class="QLabel" name="groupDumpCompressionModeLabel">
<property name="text">
<string>Dump Compression Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="gpuDumpCompressionMode"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="gpuDumpFastReplayMode">
<property name="text">
<string>Fast Dump Playback</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupBox_9"> <widget class="QGroupBox" name="groupBox_9">
<property name="title"> <property name="title">

View File

@ -61,12 +61,11 @@
LOG_CHANNEL(MainWindow); LOG_CHANNEL(MainWindow);
static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
"MainWindow", "MainWindow", "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.pbp *.exe *.psexe *.ps-exe *.psx *.psf "
"All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.pbp *.exe *.psexe *.ps-exe *.psx *.psf *.minipsf " "*.minipsf *.m3u *.psxgpu);;Single-Track Raw Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD "
"*.m3u);;Single-Track " "Images (*.chd);;Error Code Modeler Images (*.ecm);;Media Descriptor Sidecar Images "
"Raw Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images " "(*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe *.psexe *.ps-exe, "
"(*.ecm);;Media Descriptor Sidecar Images (*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe " "*.psx);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u);;PSX GPU Dumps (*.psxgpu)");
"*.psexe *.ps-exe, *.psx);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u)");
MainWindow* g_main_window = nullptr; MainWindow* g_main_window = nullptr;
@ -1158,7 +1157,7 @@ void MainWindow::promptForDiscChange(const QString& path)
SystemLock lock(pauseAndLockSystem()); SystemLock lock(pauseAndLockSystem());
bool reset_system = false; bool reset_system = false;
if (!m_was_disc_change_request) if (!m_was_disc_change_request && !System::IsGPUDumpPath(path.toStdString()))
{ {
QMessageBox mb(QMessageBox::Question, tr("Confirm Disc Change"), QMessageBox mb(QMessageBox::Question, tr("Confirm Disc Change"),
tr("Do you want to swap discs or boot the new image (via system reset)?"), QMessageBox::NoButton, tr("Do you want to swap discs or boot the new image (via system reset)?"), QMessageBox::NoButton,
@ -2002,6 +2001,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered); connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered);
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled); connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled);
connect(m_ui.actionCaptureGPUFrame, &QAction::triggered, g_emu_thread, &EmuThread::captureGPUFrameDump);
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this, connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this,
@ -2416,7 +2416,7 @@ static QString getFilenameFromMimeData(const QMimeData* md)
void MainWindow::dragEnterEvent(QDragEnterEvent* event) void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{ {
const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString()); const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString());
if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename)) if (!System::IsLoadablePath(filename) && !System::IsSaveStatePath(filename))
return; return;
event->acceptProposedAction(); event->acceptProposedAction();
@ -2426,12 +2426,12 @@ void MainWindow::dropEvent(QDropEvent* event)
{ {
const QString qfilename(getFilenameFromMimeData(event->mimeData())); const QString qfilename(getFilenameFromMimeData(event->mimeData()));
const std::string filename(qfilename.toStdString()); const std::string filename(qfilename.toStdString());
if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename)) if (!System::IsLoadablePath(filename) && !System::IsSaveStatePath(filename))
return; return;
event->acceptProposedAction(); event->acceptProposedAction();
if (System::IsSaveStateFilename(filename)) if (System::IsSaveStatePath(filename))
{ {
g_emu_thread->loadState(qfilename); g_emu_thread->loadState(qfilename);
return; return;

View File

@ -214,6 +214,7 @@
<addaction name="actionMemoryScanner"/> <addaction name="actionMemoryScanner"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionMediaCapture"/> <addaction name="actionMediaCapture"/>
<addaction name="actionCaptureGPUFrame"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionOpenTextureDirectory"/> <addaction name="actionOpenTextureDirectory"/>
<addaction name="actionReloadTextureReplacements"/> <addaction name="actionReloadTextureReplacements"/>
@ -912,6 +913,11 @@
<string>Reload Texture Replacements</string> <string>Reload Texture Replacements</string>
</property> </property>
</action> </action>
<action name="actionCaptureGPUFrame">
<property name="text">
<string>Capture GPU Frame</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="resources/duckstation-qt.qrc"/> <include location="resources/duckstation-qt.qrc"/>

View File

@ -1366,6 +1366,18 @@ void EmuThread::reloadTextureReplacements()
GPUTextureCache::ReloadTextureReplacements(true); GPUTextureCache::ReloadTextureReplacements(true);
} }
void EmuThread::captureGPUFrameDump()
{
if (!isCurrentThread())
{
QMetaObject::invokeMethod(this, "captureGPUFrameDump", Qt::QueuedConnection);
return;
}
if (System::IsValid())
System::StartRecordingGPUDump();
}
void EmuThread::runOnEmuThread(std::function<void()> callback) void EmuThread::runOnEmuThread(std::function<void()> callback)
{ {
callback(); callback();

View File

@ -210,6 +210,7 @@ public Q_SLOTS:
void updatePostProcessingSettings(); void updatePostProcessingSettings();
void clearInputBindStateFromSource(InputBindingKey key); void clearInputBindStateFromSource(InputBindingKey key);
void reloadTextureReplacements(); void reloadTextureReplacements();
void captureGPUFrameDump();
private Q_SLOTS: private Q_SLOTS:
void stopInThread(); void stopInThread();

View File

@ -659,7 +659,7 @@ SettingsWindow* SettingsWindow::openGamePropertiesDialog(const std::string& path
const char* category /* = nullptr */) const char* category /* = nullptr */)
{ {
const GameDatabase::Entry* dentry = nullptr; const GameDatabase::Entry* dentry = nullptr;
if (!System::IsExeFileName(path) && !System::IsPsfFileName(path)) if (!System::IsExePath(path) && !System::IsPsfPath(path))
{ {
// Need to resolve hash games. // Need to resolve hash games.
Error error; Error error;