mirror of
https://github.com/stenzek/duckstation.git
synced 2024-11-26 23:50:31 +00:00
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:
parent
428c3e3426
commit
4ab22921c4
@ -47,6 +47,8 @@ add_library(core
|
||||
gpu_backend.cpp
|
||||
gpu_backend.h
|
||||
gpu_commands.cpp
|
||||
gpu_dump.cpp
|
||||
gpu_dump.h
|
||||
gpu_hw.cpp
|
||||
gpu_hw.h
|
||||
gpu_hw_shadergen.cpp
|
||||
|
@ -46,6 +46,7 @@
|
||||
<ClCompile Include="gdb_server.cpp" />
|
||||
<ClCompile Include="gpu_backend.cpp" />
|
||||
<ClCompile Include="gpu_commands.cpp" />
|
||||
<ClCompile Include="gpu_dump.cpp" />
|
||||
<ClCompile Include="gpu_hw_shadergen.cpp" />
|
||||
<ClCompile Include="gpu_hw_texture_cache.cpp" />
|
||||
<ClCompile Include="gpu_shadergen.cpp" />
|
||||
@ -124,6 +125,7 @@
|
||||
<ClInclude Include="game_list.h" />
|
||||
<ClInclude Include="gdb_server.h" />
|
||||
<ClInclude Include="gpu_backend.h" />
|
||||
<ClInclude Include="gpu_dump.h" />
|
||||
<ClInclude Include="gpu_hw_shadergen.h" />
|
||||
<ClInclude Include="gpu_hw_texture_cache.h" />
|
||||
<ClInclude Include="gpu_shadergen.h" />
|
||||
|
@ -68,6 +68,7 @@
|
||||
<ClCompile Include="gpu_sw_rasterizer.cpp" />
|
||||
<ClCompile Include="gpu_hw_texture_cache.cpp" />
|
||||
<ClCompile Include="memory_scanner.cpp" />
|
||||
<ClCompile Include="gpu_dump.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="types.h" />
|
||||
@ -142,6 +143,7 @@
|
||||
<ClInclude Include="gpu_sw_rasterizer.h" />
|
||||
<ClInclude Include="gpu_hw_texture_cache.h" />
|
||||
<ClInclude Include="memory_scanner.h" />
|
||||
<ClInclude Include="gpu_dump.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="gpu_sw_rasterizer.inl" />
|
||||
|
@ -2492,6 +2492,11 @@ void CPU::ExecuteInterpreter()
|
||||
}
|
||||
}
|
||||
|
||||
fastjmp_buf* CPU::GetExecutionJmpBuf()
|
||||
{
|
||||
return &s_jmp_buf;
|
||||
}
|
||||
|
||||
void CPU::Execute()
|
||||
{
|
||||
CheckForExecutionModeChange();
|
||||
|
@ -2,9 +2,12 @@
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "bus.h"
|
||||
#include "cpu_core.h"
|
||||
|
||||
struct fastjmp_buf;
|
||||
|
||||
namespace CPU {
|
||||
|
||||
void SetPC(u32 new_pc);
|
||||
@ -28,6 +31,9 @@ ALWAYS_INLINE static void CheckForPendingInterrupt()
|
||||
|
||||
void DispatchInterrupt();
|
||||
|
||||
// access to execution jump buffer, use with care!
|
||||
fastjmp_buf* GetExecutionJmpBuf();
|
||||
|
||||
// icache stuff
|
||||
ALWAYS_INLINE static bool IsCachedAddress(VirtualMemoryAddress address)
|
||||
{
|
||||
|
@ -6,12 +6,14 @@
|
||||
#include "cdrom.h"
|
||||
#include "cpu_core.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_dump.h"
|
||||
#include "imgui.h"
|
||||
#include "interrupt_controller.h"
|
||||
#include "mdec.h"
|
||||
#include "pad.h"
|
||||
#include "spu.h"
|
||||
#include "system.h"
|
||||
#include "timing_event.h"
|
||||
|
||||
#include "util/imgui_manager.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 (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;
|
||||
for (u32 i = 0; i < word_count; i++)
|
||||
{
|
||||
|
@ -216,7 +216,7 @@ std::string GameDatabase::GetSerialForPath(const char* path)
|
||||
{
|
||||
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));
|
||||
if (image)
|
||||
|
@ -165,7 +165,7 @@ bool GameList::IsScannableFilename(std::string_view path)
|
||||
if (StringUtil::EndsWithNoCase(path, ".bin"))
|
||||
return false;
|
||||
|
||||
return System::IsLoadableFilename(path);
|
||||
return System::IsLoadablePath(path);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (System::IsExeFileName(path))
|
||||
if (System::IsExePath(path))
|
||||
return GetExeListEntry(path, entry);
|
||||
if (System::IsPsfFileName(path.c_str()))
|
||||
if (System::IsPsfPath(path.c_str()))
|
||||
return GetPsfListEntry(path, entry);
|
||||
return GetDiscListEntry(path, entry);
|
||||
}
|
||||
|
322
src/core/gpu.cpp
322
src/core/gpu.cpp
@ -3,6 +3,7 @@
|
||||
|
||||
#include "gpu.h"
|
||||
#include "dma.h"
|
||||
#include "gpu_dump.h"
|
||||
#include "gpu_shadergen.h"
|
||||
#include "gpu_sw_rasterizer.h"
|
||||
#include "host.h"
|
||||
@ -10,6 +11,7 @@
|
||||
#include "settings.h"
|
||||
#include "system.h"
|
||||
#include "timers.h"
|
||||
#include "timing_event.h"
|
||||
|
||||
#include "util/gpu_device.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,
|
||||
u32 texture_data_stride, GPUTexture::Format texture_format,
|
||||
bool display_osd_message, bool use_thread);
|
||||
static void RemoveSelfFromScreenshotThreads();
|
||||
static void JoinScreenshotThreads();
|
||||
|
||||
GPU::GPU()
|
||||
@ -86,6 +89,7 @@ GPU::~GPU()
|
||||
s_crtc_tick_event.Deactivate();
|
||||
s_frame_done_event.Deactivate();
|
||||
|
||||
StopRecordingGPUDump();
|
||||
JoinScreenshotThreads();
|
||||
DestroyDeinterlaceTextures();
|
||||
g_gpu_device->RecycleTexture(std::move(m_chroma_smoothing_texture));
|
||||
@ -93,9 +97,11 @@ GPU::~GPU()
|
||||
|
||||
bool GPU::Initialize()
|
||||
{
|
||||
if (!System::IsReplayingGPUDump())
|
||||
s_crtc_tick_event.Activate();
|
||||
|
||||
m_force_progressive_scan = (g_settings.display_deinterlacing_mode == DisplayDeinterlacingMode::Progressive);
|
||||
m_force_frame_timings = g_settings.gpu_force_video_timing;
|
||||
s_crtc_tick_event.Activate();
|
||||
m_fifo_size = g_settings.gpu_fifo_size;
|
||||
m_max_run_ahead = g_settings.gpu_max_run_ahead;
|
||||
m_console_is_pal = System::IsPALRegion();
|
||||
@ -226,7 +232,7 @@ void GPU::SoftReset()
|
||||
m_GPUSTAT.display_area_color_depth_24 = false;
|
||||
m_GPUSTAT.vertical_interlace = false;
|
||||
m_GPUSTAT.display_disable = true;
|
||||
m_GPUSTAT.dma_direction = DMADirection::Off;
|
||||
m_GPUSTAT.dma_direction = GPUDMADirection::Off;
|
||||
m_drawing_area = {};
|
||||
m_drawing_area_changed = true;
|
||||
m_drawing_offset = {};
|
||||
@ -420,19 +426,19 @@ void GPU::UpdateDMARequest()
|
||||
bool dma_request;
|
||||
switch (m_GPUSTAT.dma_direction)
|
||||
{
|
||||
case DMADirection::Off:
|
||||
case GPUDMADirection::Off:
|
||||
dma_request = false;
|
||||
break;
|
||||
|
||||
case DMADirection::FIFO:
|
||||
case GPUDMADirection::FIFO:
|
||||
dma_request = m_GPUSTAT.ready_to_recieve_dma;
|
||||
break;
|
||||
|
||||
case DMADirection::CPUtoGP0:
|
||||
case GPUDMADirection::CPUtoGP0:
|
||||
dma_request = m_GPUSTAT.ready_to_recieve_dma;
|
||||
break;
|
||||
|
||||
case DMADirection::GPUREADtoCPU:
|
||||
case GPUDMADirection::GPUREADtoCPU:
|
||||
dma_request = m_GPUSTAT.ready_to_send_vram;
|
||||
break;
|
||||
|
||||
@ -479,23 +485,35 @@ void GPU::WriteRegister(u32 offset, u32 value)
|
||||
switch (offset)
|
||||
{
|
||||
case 0x00:
|
||||
{
|
||||
if (m_gpu_dump) [[unlikely]]
|
||||
m_gpu_dump->WriteGP0Packet(value);
|
||||
|
||||
m_fifo.Push(value);
|
||||
ExecuteCommands();
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x04:
|
||||
{
|
||||
if (m_gpu_dump) [[unlikely]]
|
||||
m_gpu_dump->WriteGP1Packet(value);
|
||||
|
||||
WriteGP1(value);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
ERROR_LOG("Unhandled register write: {:02X} <- {:08X}", offset, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
std::fill_n(words, word_count, UINT32_C(0xFFFFFFFF));
|
||||
@ -877,6 +895,11 @@ TickCount GPU::GetPendingCommandTicks() const
|
||||
return SystemTicksToGPUTicks(s_command_tick_event.GetTicksSinceLastExecution());
|
||||
}
|
||||
|
||||
TickCount GPU::GetRemainingCommandTicks() const
|
||||
{
|
||||
return std::max<TickCount>(m_pending_command_ticks - GetPendingCommandTicks(), 0);
|
||||
}
|
||||
|
||||
void GPU::UpdateCRTCTickEvent()
|
||||
{
|
||||
// figure out how many GPU ticks until the next vblank or event
|
||||
@ -931,7 +954,8 @@ void GPU::UpdateCRTCTickEvent()
|
||||
ticks_until_event = std::min(ticks_until_event, ticks_until_hblank_start_or_end);
|
||||
}
|
||||
|
||||
s_crtc_tick_event.Schedule(CRTCTicksToSystemTicks(ticks_until_event, m_crtc_state.fractional_ticks));
|
||||
if (!System::IsReplayingGPUDump()) [[likely]]
|
||||
s_crtc_tick_event.Schedule(CRTCTicksToSystemTicks(ticks_until_event, m_crtc_state.fractional_ticks));
|
||||
}
|
||||
|
||||
bool GPU::IsCRTCScanlinePending() const
|
||||
@ -1030,6 +1054,13 @@ void GPU::CRTCTickEvent(TickCount ticks)
|
||||
{
|
||||
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
|
||||
// TODO: move present in here I guess
|
||||
FlushRender();
|
||||
@ -1273,7 +1304,7 @@ void GPU::WriteGP1(u32 value)
|
||||
const u32 param = value & UINT32_C(0x00FFFFFF);
|
||||
switch (command)
|
||||
{
|
||||
case 0x00: // Reset GPU
|
||||
case static_cast<u8>(GP1Command::ResetGPU):
|
||||
{
|
||||
DEBUG_LOG("GP1 reset GPU");
|
||||
s_command_tick_event.InvokeEarly();
|
||||
@ -1282,7 +1313,7 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x01: // Clear FIFO
|
||||
case static_cast<u8>(GP1Command::ClearFIFO):
|
||||
{
|
||||
DEBUG_LOG("GP1 clear FIFO");
|
||||
s_command_tick_event.InvokeEarly();
|
||||
@ -1305,7 +1336,7 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x02: // Acknowledge Interrupt
|
||||
case static_cast<u8>(GP1Command::AcknowledgeInterrupt):
|
||||
{
|
||||
DEBUG_LOG("Acknowledge interrupt");
|
||||
m_GPUSTAT.interrupt_request = false;
|
||||
@ -1313,7 +1344,7 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x03: // Display on/off
|
||||
case static_cast<u8>(GP1Command::SetDisplayDisable):
|
||||
{
|
||||
const bool disable = ConvertToBoolUnchecked(value & 0x01);
|
||||
DEBUG_LOG("Display {}", disable ? "disabled" : "enabled");
|
||||
@ -1326,18 +1357,18 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x04: // DMA Direction
|
||||
case static_cast<u8>(GP1Command::SetDMADirection):
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x05: // Set display start address
|
||||
case static_cast<u8>(GP1Command::SetDisplayStartAddress):
|
||||
{
|
||||
const u32 new_value = param & CRTCState::Regs::DISPLAY_ADDRESS_START_MASK;
|
||||
DEBUG_LOG("Display address start <- 0x{:08X}", new_value);
|
||||
@ -1353,7 +1384,7 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x06: // Set horizontal display range
|
||||
case static_cast<u8>(GP1Command::SetHorizontalDisplayRange):
|
||||
{
|
||||
const u32 new_value = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK;
|
||||
DEBUG_LOG("Horizontal display range <- 0x{:08X}", new_value);
|
||||
@ -1367,7 +1398,7 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x07: // Set vertical display range
|
||||
case static_cast<u8>(GP1Command::SetVerticalDisplayRange):
|
||||
{
|
||||
const u32 new_value = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK;
|
||||
DEBUG_LOG("Vertical display range <- 0x{:08X}", new_value);
|
||||
@ -1381,22 +1412,9 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x08: // Set display mode
|
||||
case static_cast<u8>(GP1Command::SetDisplayMode):
|
||||
{
|
||||
union GP1_08h
|
||||
{
|
||||
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};
|
||||
const GP1SetDisplayMode dm{param};
|
||||
GPUSTAT new_GPUSTAT{m_GPUSTAT.bits};
|
||||
new_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1;
|
||||
new_GPUSTAT.vertical_resolution = dm.vertical_resolution;
|
||||
@ -1425,7 +1443,7 @@ void GPU::WriteGP1(u32 value)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x09: // Allow texture disable
|
||||
case static_cast<u8>(GP1Command::SetAllowTextureDisable):
|
||||
{
|
||||
m_set_texture_disable_mask = ConvertToBoolUnchecked(param & 0x01);
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveSelfFromScreenshotThreads();
|
||||
|
||||
return result;
|
||||
};
|
||||
@ -2502,6 +2507,21 @@ bool CompressAndWriteTextureToFile(u32 width, u32 height, std::string filename,
|
||||
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()
|
||||
{
|
||||
std::unique_lock lock(s_screenshot_threads_mutex);
|
||||
@ -2886,3 +2906,209 @@ void GPU::UpdateStatistics(u32 frame_count)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
|
||||
#include "gpu_types.h"
|
||||
#include "timers.h"
|
||||
#include "timing_event.h"
|
||||
#include "types.h"
|
||||
|
||||
#include "util/gpu_device.h"
|
||||
@ -20,9 +19,11 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
class Error;
|
||||
class SmallStringBase;
|
||||
|
||||
class StateWrapper;
|
||||
@ -32,6 +33,11 @@ class GPUTexture;
|
||||
class GPUPipeline;
|
||||
class MediaCapture;
|
||||
|
||||
namespace GPUDump {
|
||||
enum class PacketType : u8;
|
||||
class Recorder;
|
||||
class Player;
|
||||
}
|
||||
struct Settings;
|
||||
|
||||
namespace Threading {
|
||||
@ -49,14 +55,6 @@ public:
|
||||
DrawingPolyLine
|
||||
};
|
||||
|
||||
enum class DMADirection : u32
|
||||
{
|
||||
Off = 0,
|
||||
FIFO = 1,
|
||||
CPUtoGP0 = 2,
|
||||
GPUREADtoCPU = 3
|
||||
};
|
||||
|
||||
enum : u32
|
||||
{
|
||||
MAX_FIFO_SIZE = 4096,
|
||||
@ -120,7 +118,7 @@ public:
|
||||
|
||||
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)
|
||||
{
|
||||
@ -128,6 +126,13 @@ public:
|
||||
}
|
||||
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.
|
||||
ALWAYS_INLINE bool IsDisplayDisabled() const
|
||||
{
|
||||
@ -152,6 +157,7 @@ public:
|
||||
/// Returns the number of pending GPU ticks.
|
||||
TickCount GetPendingCRTCTicks() const;
|
||||
TickCount GetPendingCommandTicks() const;
|
||||
TickCount GetRemainingCommandTicks() const;
|
||||
|
||||
/// Returns true if enough ticks have passed for the raster to be on the next line.
|
||||
bool IsCRTCScanlinePending() const;
|
||||
@ -414,54 +420,7 @@ protected:
|
||||
AddCommandTicks(std::max(drawn_width, drawn_height));
|
||||
}
|
||||
|
||||
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, 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 = {};
|
||||
GPUSTAT m_GPUSTAT = {};
|
||||
|
||||
struct DrawMode
|
||||
{
|
||||
@ -606,6 +565,8 @@ protected:
|
||||
u32 m_blit_remaining_words;
|
||||
GPURenderCommand m_render_command{};
|
||||
|
||||
std::unique_ptr<GPUDump::Recorder> m_gpu_dump;
|
||||
|
||||
ALWAYS_INLINE u32 FifoPop() { return Truncate32(m_fifo.Pop()); }
|
||||
ALWAYS_INLINE u32 FifoPeek() { return Truncate32(m_fifo.Peek()); }
|
||||
ALWAYS_INLINE u32 FifoPeek(u32 i) { return Truncate32(m_fifo.Peek(i)); }
|
||||
|
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "gpu.h"
|
||||
#include "gpu_dump.h"
|
||||
#include "gpu_hw_texture_cache.h"
|
||||
#include "interrupt_controller.h"
|
||||
#include "system.h"
|
||||
@ -604,6 +605,11 @@ bool GPU::HandleCopyRectangleVRAMToCPUCommand()
|
||||
m_counters.num_reads++;
|
||||
m_blitter_state = BlitterState::ReadingVRAM;
|
||||
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;
|
||||
}
|
||||
|
||||
|
534
src/core/gpu_dump.cpp
Normal file
534
src/core/gpu_dump.cpp
Normal 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
143
src/core/gpu_dump.h
Normal 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
|
@ -44,6 +44,14 @@ enum : s32
|
||||
MAX_PRIMITIVE_HEIGHT = 512,
|
||||
};
|
||||
|
||||
enum class GPUDMADirection : u8
|
||||
{
|
||||
Off = 0,
|
||||
FIFO = 1,
|
||||
CPUtoGP0 = 2,
|
||||
GPUREADtoCPU = 3
|
||||
};
|
||||
|
||||
enum class GPUPrimitive : u8
|
||||
{
|
||||
Reserved = 0,
|
||||
@ -92,6 +100,20 @@ enum class GPUInterlacedDisplayMode : u8
|
||||
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.
|
||||
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)
|
||||
{
|
||||
// Helper/format conversion functions - constants from https://stackoverflow.com/a/9069480
|
||||
|
@ -224,6 +224,20 @@ DEFINE_HOTKEY("Screenshot", TRANSLATE_NOOP("Hotkeys", "General"), TRANSLATE_NOOP
|
||||
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__
|
||||
DEFINE_HOTKEY("ToggleMediaCapture", TRANSLATE_NOOP("Hotkeys", "General"),
|
||||
TRANSLATE_NOOP("Hotkeys", "Toggle Media Capture"), [](s32 pressed) {
|
||||
|
@ -248,6 +248,7 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si)
|
||||
gpu_pgxp_depth_buffer = si.GetBoolValue("GPU", "PGXPDepthBuffer", false);
|
||||
gpu_pgxp_disable_2d = si.GetBoolValue("GPU", "PGXPDisableOn2DPolygons", false);
|
||||
SetPGXPDepthClearThreshold(si.GetFloatValue("GPU", "PGXPDepthClearThreshold", DEFAULT_GPU_PGXP_DEPTH_THRESHOLD));
|
||||
gpu_dump_fast_replay_mode = si.GetBoolValue("GPU", "DumpFastReplayMode", false);
|
||||
|
||||
display_deinterlacing_mode =
|
||||
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", "PGXPDisableOn2DPolygons", gpu_pgxp_disable_2d);
|
||||
si.SetFloatValue("GPU", "PGXPDepthClearThreshold", GetPGXPDepthClearThreshold());
|
||||
si.SetBoolValue("GPU", "DumpFastReplayMode", gpu_dump_fast_replay_mode);
|
||||
|
||||
si.SetStringValue("GPU", "DeinterlacingMode", GetDisplayDeinterlacingModeName(display_deinterlacing_mode));
|
||||
si.SetStringValue("Display", "CropMode", GetDisplayCropModeName(display_crop_mode));
|
||||
@ -1519,6 +1521,42 @@ const char* Settings::GetGPUWireframeModeDisplayName(GPUWireframeMode mode)
|
||||
"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 = {
|
||||
"Disabled", "Weave", "Blend", "Adaptive", "Progressive",
|
||||
};
|
||||
|
@ -279,6 +279,7 @@ struct Settings
|
||||
bool bios_patch_fast_boot : 1 = DEFAULT_FAST_BOOT_VALUE;
|
||||
bool bios_fast_forward_boot : 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<MemoryCardType, NUM_CONTROLLER_AND_CARD_PORTS> memory_card_types{};
|
||||
@ -423,6 +424,10 @@ struct Settings
|
||||
static const char* GetGPUWireframeModeName(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 const char* GetDisplayDeinterlacingModeName(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 GPUDownsampleMode DEFAULT_GPU_DOWNSAMPLE_MODE = GPUDownsampleMode::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 float DEFAULT_GPU_PGXP_DEPTH_THRESHOLD = 300.0f;
|
||||
static constexpr float GPU_PGXP_DEPTH_THRESHOLD_SCALE = 4096.0f;
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "game_database.h"
|
||||
#include "game_list.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_dump.h"
|
||||
#include "gpu_hw_texture_cache.h"
|
||||
#include "gte.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 void HandleHostGPUDeviceLost();
|
||||
static void HandleExclusiveFullscreenLost();
|
||||
static std::string GetScreenshotPath(const char* extension);
|
||||
|
||||
/// Returns true if boot is being fast forwarded.
|
||||
static bool IsFastForwardingBoot();
|
||||
@ -224,6 +226,9 @@ static void DoRewind();
|
||||
static void SaveRunaheadState();
|
||||
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);
|
||||
|
||||
#ifdef ENABLE_DISCORD_PRESENCE
|
||||
@ -320,6 +325,7 @@ static Common::Timer s_frame_timer;
|
||||
static Threading::ThreadHandle s_cpu_thread_handle;
|
||||
|
||||
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
|
||||
static std::optional<System::SaveStateBuffer> s_undo_load_state;
|
||||
@ -631,6 +637,11 @@ bool System::IsExecuting()
|
||||
return s_system_executing;
|
||||
}
|
||||
|
||||
bool System::IsReplayingGPUDump()
|
||||
{
|
||||
return static_cast<bool>(s_gpu_dump_player);
|
||||
}
|
||||
|
||||
bool System::IsStartupCancelled()
|
||||
{
|
||||
return s_startup_cancelled.load();
|
||||
@ -845,24 +856,30 @@ u32 System::GetFrameTimeHistoryPos()
|
||||
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") ||
|
||||
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"));
|
||||
}
|
||||
|
||||
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 = {
|
||||
".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs
|
||||
".exe", ".psexe", ".ps-exe", ".psx", // exes
|
||||
".psf", ".minipsf", // psf
|
||||
".m3u", // playlists
|
||||
".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs
|
||||
".exe", ".psexe", ".ps-exe", ".psx", // exes
|
||||
".psf", ".minipsf", // psf
|
||||
".psxgpu", ".psxgpu.zst", // gpu dump
|
||||
".m3u", // playlists
|
||||
".pbp",
|
||||
};
|
||||
|
||||
@ -875,7 +892,7 @@ bool System::IsLoadableFilename(std::string_view path)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool System::IsSaveStateFilename(std::string_view path)
|
||||
bool System::IsSaveStatePath(std::string_view path)
|
||||
{
|
||||
return StringUtil::EndsWithNoCase(path, ".sav");
|
||||
}
|
||||
@ -1556,7 +1573,8 @@ bool System::UpdateGameSettingsLayer()
|
||||
s_input_settings_interface = std::move(input_interface);
|
||||
s_input_profile_name = std::move(input_profile_name);
|
||||
|
||||
Cheats::ReloadCheats(false, true, false, true);
|
||||
if (!IsReplayingGPUDump())
|
||||
Cheats::ReloadCheats(false, true, false, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -1693,16 +1711,29 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
std::string exe_override;
|
||||
if (!parameters.filename.empty())
|
||||
{
|
||||
if (IsExeFileName(parameters.filename))
|
||||
if (IsExePath(parameters.filename))
|
||||
{
|
||||
boot_mode = BootMode::BootEXE;
|
||||
exe_override = parameters.filename;
|
||||
}
|
||||
else if (IsPsfFileName(parameters.filename))
|
||||
else if (IsPsfPath(parameters.filename))
|
||||
{
|
||||
boot_mode = BootMode::BootPSF;
|
||||
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 (s_region == ConsoleRegion::Auto)
|
||||
@ -1714,7 +1745,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
s_region = GetConsoleRegionForDiscRegion(file_region);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (boot_mode != BootMode::ReplayGPUDump)
|
||||
{
|
||||
INFO_LOG("Loading CD image '{}'...", Path::GetFileName(parameters.filename));
|
||||
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,
|
||||
Path::GetFileName(parameters.filename));
|
||||
s_state = State::Shutdown;
|
||||
s_gpu_dump_player.reset();
|
||||
Host::OnSystemDestroyed();
|
||||
Host::OnIdleStateChanged();
|
||||
return false;
|
||||
@ -1781,11 +1813,12 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
// Get boot EXE override.
|
||||
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.",
|
||||
Path::GetFileName(parameters.override_exe));
|
||||
s_state = State::Shutdown;
|
||||
s_gpu_dump_player.reset();
|
||||
Cheats::UnloadAll();
|
||||
ClearRunningGame();
|
||||
Host::OnSystemDestroyed();
|
||||
@ -1802,6 +1835,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
if (!CheckForSBIFile(disc.get(), error))
|
||||
{
|
||||
s_state = State::Shutdown;
|
||||
s_gpu_dump_player.reset();
|
||||
Cheats::UnloadAll();
|
||||
ClearRunningGame();
|
||||
Host::OnSystemDestroyed();
|
||||
@ -1840,6 +1874,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
if (cancelled)
|
||||
{
|
||||
s_state = State::Shutdown;
|
||||
s_gpu_dump_player.reset();
|
||||
Cheats::UnloadAll();
|
||||
ClearRunningGame();
|
||||
Host::OnSystemDestroyed();
|
||||
@ -1854,6 +1889,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
if (!SetBootMode(boot_mode, disc_region, error))
|
||||
{
|
||||
s_state = State::Shutdown;
|
||||
s_gpu_dump_player.reset();
|
||||
Cheats::UnloadAll();
|
||||
ClearRunningGame();
|
||||
Host::OnSystemDestroyed();
|
||||
@ -1867,6 +1903,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
|
||||
{
|
||||
s_boot_mode = System::BootMode::None;
|
||||
s_state = State::Shutdown;
|
||||
s_gpu_dump_player.reset();
|
||||
Cheats::UnloadAll();
|
||||
ClearRunningGame();
|
||||
Host::OnSystemDestroyed();
|
||||
@ -2038,6 +2075,8 @@ void System::DestroySystem()
|
||||
|
||||
ImGuiManager::DestroyAllDebugWindows();
|
||||
|
||||
s_gpu_dump_player.reset();
|
||||
|
||||
s_undo_load_state.reset();
|
||||
|
||||
#ifdef ENABLE_GDB_SERVER
|
||||
@ -2133,7 +2172,9 @@ void System::Execute()
|
||||
g_gpu->RestoreDeviceContext();
|
||||
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();
|
||||
else
|
||||
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.
|
||||
// TODO: when running ahead, we can skip this (and the flush above)
|
||||
SPU::GeneratePendingSamples();
|
||||
if (!IsReplayingGPUDump()) [[likely]]
|
||||
{
|
||||
SPU::GeneratePendingSamples();
|
||||
|
||||
Cheats::ApplyFrameEndCodes();
|
||||
Cheats::ApplyFrameEndCodes();
|
||||
|
||||
if (Achievements::IsActive())
|
||||
Achievements::FrameUpdate();
|
||||
if (Achievements::IsActive())
|
||||
Achievements::FrameUpdate();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DISCORD_PRESENCE
|
||||
PollDiscordPresence();
|
||||
@ -2697,7 +2741,7 @@ bool System::SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, Error*
|
||||
return true;
|
||||
|
||||
// Need to reload the BIOS to wipe out the patching.
|
||||
if (!LoadBIOS(error))
|
||||
if (new_boot_mode != BootMode::ReplayGPUDump && !LoadBIOS(error))
|
||||
return false;
|
||||
|
||||
// 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
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."));
|
||||
return false;
|
||||
@ -3780,7 +3829,7 @@ void System::ResetControllers()
|
||||
std::unique_ptr<MemoryCard> System::GetMemoryCardForSlot(u32 slot, MemoryCardType type)
|
||||
{
|
||||
// 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)
|
||||
return nullptr;
|
||||
|
||||
@ -4070,6 +4119,9 @@ std::string System::GetMediaFileName()
|
||||
|
||||
bool System::InsertMedia(const char* path)
|
||||
{
|
||||
if (IsGPUDumpPath(path)) [[unlikely]]
|
||||
return ChangeGPUDump(path);
|
||||
|
||||
Error error;
|
||||
std::unique_ptr<CDImage> image = CDImage::Open(path, g_settings.cdrom_load_image_patches, &error);
|
||||
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_custom_title = !s_running_game_title.empty();
|
||||
|
||||
if (IsExeFileName(path))
|
||||
if (IsExePath(path))
|
||||
{
|
||||
if (s_running_game_title.empty())
|
||||
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)
|
||||
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.
|
||||
if (s_running_game_title.empty())
|
||||
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.
|
||||
else if (image && image->GetTrack(1).mode != CDImage::TrackMode::Audio)
|
||||
{
|
||||
@ -4180,13 +4248,17 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
|
||||
if (!booting)
|
||||
GPUTextureCache::SetGameID(s_running_game_serial);
|
||||
|
||||
if (booting)
|
||||
Achievements::ResetHardcoreMode(true);
|
||||
if (!IsReplayingGPUDump())
|
||||
{
|
||||
if (booting)
|
||||
Achievements::ResetHardcoreMode(true);
|
||||
|
||||
Achievements::GameChanged(s_running_game_path, image);
|
||||
Achievements::GameChanged(s_running_game_path, image);
|
||||
|
||||
// game layer reloads cheats, but only the active list, we need new files
|
||||
Cheats::ReloadCheats(true, false, false, true);
|
||||
}
|
||||
|
||||
// game layer reloads cheats, but only the active list, we need new files
|
||||
Cheats::ReloadCheats(true, false, false, true);
|
||||
UpdateGameSettingsLayer();
|
||||
|
||||
ApplySettings(true);
|
||||
@ -4879,6 +4951,14 @@ void System::UpdateMemorySaveStateSettings()
|
||||
{
|
||||
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;
|
||||
|
||||
if (g_settings.rewind_enable)
|
||||
@ -5259,38 +5339,60 @@ void System::UpdateVolume()
|
||||
SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume());
|
||||
}
|
||||
|
||||
bool System::SaveScreenshot(const char* filename, DisplayScreenshotMode mode, DisplayScreenshotFormat format,
|
||||
u8 quality, bool compress_on_thread)
|
||||
std::string System::GetScreenshotPath(const char* extension)
|
||||
{
|
||||
if (!System::IsValid())
|
||||
return false;
|
||||
const std::string sanitized_name = Path::SanitizeFileName(System::GetGameTitle());
|
||||
std::string basename;
|
||||
if (sanitized_name.empty())
|
||||
basename = fmt::format("{}", GetTimestampStringForFileName());
|
||||
else
|
||||
basename = fmt::format("{} {}", sanitized_name, GetTimestampStringForFileName());
|
||||
|
||||
std::string auto_filename;
|
||||
if (!filename)
|
||||
std::string path = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.{}", EmuFolders::Screenshots, basename, extension);
|
||||
|
||||
// handle quick screenshots to the same filename
|
||||
u32 next_suffix = 1;
|
||||
while (FileSystem::FileExists(path.c_str()))
|
||||
{
|
||||
const std::string sanitized_name = Path::SanitizeFileName(System::GetGameTitle());
|
||||
const char* extension = Settings::GetDisplayScreenshotFormatExtension(format);
|
||||
std::string basename;
|
||||
if (sanitized_name.empty())
|
||||
basename = fmt::format("{}", GetTimestampStringForFileName());
|
||||
else
|
||||
basename = fmt::format("{} {}", sanitized_name, GetTimestampStringForFileName());
|
||||
|
||||
auto_filename = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.{}", EmuFolders::Screenshots, basename, extension);
|
||||
|
||||
// handle quick screenshots to the same filename
|
||||
u32 next_suffix = 1;
|
||||
while (FileSystem::FileExists(auto_filename.c_str()))
|
||||
{
|
||||
auto_filename = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{} ({}).{}", EmuFolders::Screenshots, basename,
|
||||
next_suffix, extension);
|
||||
next_suffix++;
|
||||
}
|
||||
|
||||
filename = auto_filename.c_str();
|
||||
path =
|
||||
fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{} ({}).{}", EmuFolders::Screenshots, basename, next_suffix, extension);
|
||||
next_suffix++;
|
||||
}
|
||||
|
||||
return g_gpu->RenderScreenshotToFile(filename, mode, quality, compress_on_thread, true);
|
||||
return path;
|
||||
}
|
||||
|
||||
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)
|
||||
@ -5833,6 +5935,34 @@ void System::InvalidateDisplay()
|
||||
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)
|
||||
{
|
||||
const u64 ctime = Common::Timer::GetCurrentValue();
|
||||
|
@ -102,6 +102,7 @@ enum class BootMode
|
||||
FastBoot,
|
||||
BootEXE,
|
||||
BootPSF,
|
||||
ReplayGPUDump,
|
||||
};
|
||||
|
||||
enum class Taint : u8
|
||||
@ -118,17 +119,20 @@ enum class Taint : u8
|
||||
|
||||
extern TickCount g_ticks_per_second;
|
||||
|
||||
/// Returns true if the filename is a PlayStation executable we can inject.
|
||||
bool IsExeFileName(std::string_view path);
|
||||
/// Returns true if the path is a PlayStation executable we can inject.
|
||||
bool IsExePath(std::string_view path);
|
||||
|
||||
/// Returns true if the filename is a Portable Sound Format file we can uncompress/load.
|
||||
bool IsPsfFileName(std::string_view path);
|
||||
/// Returns true if the path is a Portable Sound Format file we can uncompress/load.
|
||||
bool IsPsfPath(std::string_view path);
|
||||
|
||||
/// Returns true if the filename is one we can load.
|
||||
bool IsLoadableFilename(std::string_view path);
|
||||
/// Returns true if the path is a GPU dump that we can replay.
|
||||
bool IsGPUDumpPath(std::string_view path);
|
||||
|
||||
/// Returns true if the filename is a save state.
|
||||
bool IsSaveStateFilename(std::string_view path);
|
||||
/// Returns true if the path is one we can load.
|
||||
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.
|
||||
ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region);
|
||||
@ -159,6 +163,7 @@ bool IsShutdown();
|
||||
bool IsValid();
|
||||
bool IsValidOrInitializing();
|
||||
bool IsExecuting();
|
||||
bool IsReplayingGPUDump();
|
||||
|
||||
bool IsStartupCancelled();
|
||||
void CancelPendingStartup();
|
||||
@ -390,10 +395,14 @@ s32 GetAudioOutputVolume();
|
||||
void UpdateVolume();
|
||||
|
||||
/// 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,
|
||||
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.
|
||||
std::string GetNewMediaCapturePath(const std::string_view title, const std::string_view container);
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include "gpu.h"
|
||||
#include "interrupt_controller.h"
|
||||
#include "system.h"
|
||||
#include "timing_event.h"
|
||||
|
||||
#include "util/imgui_manager.h"
|
||||
#include "util/state_wrapper.h"
|
||||
@ -74,7 +75,7 @@ static void UpdateSysClkEvent();
|
||||
namespace {
|
||||
struct TimersState
|
||||
{
|
||||
TimingEvent sysclk_event{ "Timer SysClk Interrupt", 1, 1, &Timers::AddSysClkTicks, nullptr };
|
||||
TimingEvent sysclk_event{"Timer SysClk Interrupt", 1, 1, &Timers::AddSysClkTicks, nullptr};
|
||||
|
||||
std::array<CounterState, NUM_TIMERS> counters{};
|
||||
TickCount sysclk_ticks_carry = 0; // 0 unless overclocking is enabled
|
||||
|
@ -88,6 +88,11 @@ TimingEvent** TimingEvents::GetHeadEventPtr()
|
||||
return &s_state.active_events_head;
|
||||
}
|
||||
|
||||
void TimingEvents::SetGlobalTickCounter(GlobalTicks ticks)
|
||||
{
|
||||
s_state.global_tick_counter = ticks;
|
||||
}
|
||||
|
||||
void TimingEvents::SortEvent(TimingEvent* event)
|
||||
{
|
||||
const GlobalTicks event_runtime = event->m_next_run_time;
|
||||
|
@ -94,4 +94,7 @@ void UpdateCPUDowncount();
|
||||
|
||||
TimingEvent** GetHeadEventPtr();
|
||||
|
||||
// Tick counter injection, only for GPU dump replayer.
|
||||
void SetGlobalTickCounter(GlobalTicks ticks);
|
||||
|
||||
} // namespace TimingEvents
|
@ -126,6 +126,16 @@ enum class GPULineDetectMode : u8
|
||||
Count
|
||||
};
|
||||
|
||||
enum class GPUDumpCompressionMode : u8
|
||||
{
|
||||
Disabled,
|
||||
ZstLow,
|
||||
ZstDefault,
|
||||
ZstHigh,
|
||||
// TODO: XZ
|
||||
MaxCount
|
||||
};
|
||||
|
||||
enum class DisplayCropMode : u8
|
||||
{
|
||||
None,
|
||||
|
@ -294,6 +294,12 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
|
||||
|
||||
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.disableShaderCache, "GPU", "DisableShaderCache", false);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableDualSource, "GPU", "DisableDualSourceBlend", false);
|
||||
|
@ -1297,6 +1297,32 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</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>
|
||||
<widget class="QGroupBox" name="groupBox_9">
|
||||
<property name="title">
|
||||
|
@ -61,12 +61,11 @@
|
||||
LOG_CHANNEL(MainWindow);
|
||||
|
||||
static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
|
||||
"MainWindow",
|
||||
"All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.pbp *.exe *.psexe *.ps-exe *.psx *.psf *.minipsf "
|
||||
"*.m3u);;Single-Track "
|
||||
"Raw Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images "
|
||||
"(*.ecm);;Media Descriptor Sidecar Images (*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe "
|
||||
"*.psexe *.ps-exe, *.psx);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u)");
|
||||
"MainWindow", "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.pbp *.exe *.psexe *.ps-exe *.psx *.psf "
|
||||
"*.minipsf *.m3u *.psxgpu);;Single-Track Raw Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD "
|
||||
"Images (*.chd);;Error Code Modeler Images (*.ecm);;Media Descriptor Sidecar Images "
|
||||
"(*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe *.psexe *.ps-exe, "
|
||||
"*.psx);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u);;PSX GPU Dumps (*.psxgpu)");
|
||||
|
||||
MainWindow* g_main_window = nullptr;
|
||||
|
||||
@ -1158,7 +1157,7 @@ void MainWindow::promptForDiscChange(const QString& path)
|
||||
SystemLock lock(pauseAndLockSystem());
|
||||
|
||||
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"),
|
||||
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.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
|
||||
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.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
|
||||
connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this,
|
||||
@ -2416,7 +2416,7 @@ static QString getFilenameFromMimeData(const QMimeData* md)
|
||||
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
|
||||
{
|
||||
const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString());
|
||||
if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename))
|
||||
if (!System::IsLoadablePath(filename) && !System::IsSaveStatePath(filename))
|
||||
return;
|
||||
|
||||
event->acceptProposedAction();
|
||||
@ -2426,12 +2426,12 @@ void MainWindow::dropEvent(QDropEvent* event)
|
||||
{
|
||||
const QString qfilename(getFilenameFromMimeData(event->mimeData()));
|
||||
const std::string filename(qfilename.toStdString());
|
||||
if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename))
|
||||
if (!System::IsLoadablePath(filename) && !System::IsSaveStatePath(filename))
|
||||
return;
|
||||
|
||||
event->acceptProposedAction();
|
||||
|
||||
if (System::IsSaveStateFilename(filename))
|
||||
if (System::IsSaveStatePath(filename))
|
||||
{
|
||||
g_emu_thread->loadState(qfilename);
|
||||
return;
|
||||
|
@ -214,6 +214,7 @@
|
||||
<addaction name="actionMemoryScanner"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionMediaCapture"/>
|
||||
<addaction name="actionCaptureGPUFrame"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionOpenTextureDirectory"/>
|
||||
<addaction name="actionReloadTextureReplacements"/>
|
||||
@ -912,6 +913,11 @@
|
||||
<string>Reload Texture Replacements</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCaptureGPUFrame">
|
||||
<property name="text">
|
||||
<string>Capture GPU Frame</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/duckstation-qt.qrc"/>
|
||||
|
@ -1366,6 +1366,18 @@ void EmuThread::reloadTextureReplacements()
|
||||
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)
|
||||
{
|
||||
callback();
|
||||
|
@ -210,6 +210,7 @@ public Q_SLOTS:
|
||||
void updatePostProcessingSettings();
|
||||
void clearInputBindStateFromSource(InputBindingKey key);
|
||||
void reloadTextureReplacements();
|
||||
void captureGPUFrameDump();
|
||||
|
||||
private Q_SLOTS:
|
||||
void stopInThread();
|
||||
|
@ -659,7 +659,7 @@ SettingsWindow* SettingsWindow::openGamePropertiesDialog(const std::string& path
|
||||
const char* category /* = 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.
|
||||
Error error;
|
||||
|
Loading…
Reference in New Issue
Block a user