GS: Refactor screenshots/GS dumping triggers

i.e. make it not rubbish and a massive race condition.

Also adds GS dump saving to Qt.
This commit is contained in:
Connor McLaughlin 2022-05-16 21:10:34 +10:00 committed by refractionpcsx2
parent 7bd7cdd867
commit 8c270288de
18 changed files with 268 additions and 130 deletions

View File

@ -611,6 +611,22 @@ void EmuThread::runOnCPUThread(const std::function<void()>& func)
func(); func();
} }
void EmuThread::queueSnapshot(quint32 gsdump_frames)
{
if (!isOnEmuThread())
{
QMetaObject::invokeMethod(this, "queueSnapshot", Qt::QueuedConnection, Q_ARG(quint32, gsdump_frames));
return;
}
if (!VMManager::HasValidVM())
return;
GetMTGS().RunOnGSThread([gsdump_frames]() {
GSQueueSnapshot(std::string(), gsdump_frames);
});
}
void EmuThread::updateDisplay() void EmuThread::updateDisplay()
{ {
pxAssertRel(!isOnEmuThread(), "Not on emu thread"); pxAssertRel(!isOnEmuThread(), "Not on emu thread");
@ -893,13 +909,6 @@ SysMtgsThread& GetMTGS()
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
BEGIN_HOTKEY_LIST(g_host_hotkeys) BEGIN_HOTKEY_LIST(g_host_hotkeys)
DEFINE_HOTKEY("Screenshot", "General", "Save Screenshot", [](bool pressed) {
if (!pressed)
{
Host::AddOSDMessage("Saved Screenshot.", 10.0f);
GSmakeSnapshot(EmuFolders::Snapshots.ToString().char_str());
}
})
DEFINE_HOTKEY("ShutdownVM", "System", "Shut Down Virtual Machine", [](bool pressed) { DEFINE_HOTKEY("ShutdownVM", "System", "Shut Down Virtual Machine", [](bool pressed) {
if (!pressed) if (!pressed)
{ {

View File

@ -79,6 +79,7 @@ public Q_SLOTS:
void enumerateInputDevices(); void enumerateInputDevices();
void enumerateVibrationMotors(); void enumerateVibrationMotors();
void runOnCPUThread(const std::function<void()>& func); void runOnCPUThread(const std::function<void()>& func);
void queueSnapshot(quint32 gsdump_frames);
Q_SIGNALS: Q_SIGNALS:
DisplayWidget* onCreateDisplayRequested(bool fullscreen, bool render_to_main); DisplayWidget* onCreateDisplayRequested(bool fullscreen, bool render_to_main);

View File

@ -45,8 +45,6 @@
#include "Settings/InterfaceSettingsWidget.h" #include "Settings/InterfaceSettingsWidget.h"
#include "SettingWidgetBinder.h" #include "SettingWidgetBinder.h"
extern u32 GSmakeSnapshot(char* path);
static constexpr char DISC_IMAGE_FILTER[] = static constexpr char DISC_IMAGE_FILTER[] =
QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.gz *.elf *.irx *.m3u *.gs *.gs.xz);;" QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.gz *.elf *.irx *.m3u *.gs *.gs.xz);;"
"Single-Track Raw Images (*.bin *.iso);;" "Single-Track Raw Images (*.bin *.iso);;"
@ -212,6 +210,8 @@ void MainWindow::connectSignals()
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableIOPConsoleLogging, "Logging", "EnableIOPConsole", true); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableIOPConsoleLogging, "Logging", "EnableIOPConsole", true);
connect(m_ui.actionEnableIOPConsoleLogging, &QAction::triggered, this, &MainWindow::onLoggingOptionChanged); connect(m_ui.actionEnableIOPConsoleLogging, &QAction::triggered, this, &MainWindow::onLoggingOptionChanged);
connect(m_ui.actionSaveGSDump, &QAction::triggered, this, &MainWindow::onSaveGSDumpActionTriggered);
// These need to be queued connections to stop crashing due to menus opening/closing and switching focus. // These need to be queued connections to stop crashing due to menus opening/closing and switching focus.
connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress); connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress);
connect(m_game_list_widget, &GameListWidget::refreshComplete, this, &MainWindow::onGameListRefreshComplete); connect(m_game_list_widget, &GameListWidget::refreshComplete, this, &MainWindow::onGameListRefreshComplete);
@ -514,8 +514,12 @@ void MainWindow::setIconThemeFromSettings()
void MainWindow::onScreenshotActionTriggered() void MainWindow::onScreenshotActionTriggered()
{ {
Host::AddOSDMessage("Saved Screenshot.", 10.0f); g_emu_thread->queueSnapshot(0);
GSmakeSnapshot(EmuFolders::Snapshots.ToString().char_str()); }
void MainWindow::onSaveGSDumpActionTriggered()
{
g_emu_thread->queueSnapshot(1);
} }
void MainWindow::saveStateToConfig() void MainWindow::saveStateToConfig()

View File

@ -134,6 +134,7 @@ private Q_SLOTS:
void onThemeChangedFromSettings(); void onThemeChangedFromSettings();
void onLoggingOptionChanged(); void onLoggingOptionChanged();
void onScreenshotActionTriggered(); void onScreenshotActionTriggered();
void onSaveGSDumpActionTriggered();
void onVMStarting(); void onVMStarting();
void onVMStarted(); void onVMStarted();

View File

@ -136,8 +136,9 @@
</property> </property>
</widget> </widget>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionToggleSoftwareRendering"/>
<addaction name="menuDebugSwitchRenderer"/> <addaction name="menuDebugSwitchRenderer"/>
<addaction name="actionToggleSoftwareRendering"/>
<addaction name="actionSaveGSDump"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionReloadPatches"/> <addaction name="actionReloadPatches"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -561,12 +562,13 @@
</property> </property>
</action> </action>
<action name="actionDEV9Settings"> <action name="actionDEV9Settings">
<property name="icon">
<iconset theme="dashboard-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text"> <property name="text">
<string>&amp;Network &amp;&amp; HDD</string> <string>&amp;Network &amp;&amp; HDD</string>
</property> </property>
<property name="icon">
<iconset theme="dashboard-line"/>
</property>
</action> </action>
<action name="actionViewToolbar"> <action name="actionViewToolbar">
<property name="checkable"> <property name="checkable">
@ -738,6 +740,11 @@
<string>Enable IOP Console Logging</string> <string>Enable IOP Console Logging</string>
</property> </property>
</action> </action>
<action name="actionSaveGSDump">
<property name="text">
<string>Save Single Frame GS Dump</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="resources/resources.qrc"/> <include location="resources/resources.qrc"/>

View File

@ -208,6 +208,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget*
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useDebugDevice, "EmuCore/GS", "UseDebugDevice", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useDebugDevice, "EmuCore/GS", "UseDebugDevice", false);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.overrideTextureBarriers, "EmuCore/GS", "OverrideTextureBarriers", -1, -1); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.overrideTextureBarriers, "EmuCore/GS", "OverrideTextureBarriers", -1, -1);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.overrideGeometryShader, "EmuCore/GS", "OverrideGeometryShaders", -1, -1); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.overrideGeometryShader, "EmuCore/GS", "OverrideGeometryShaders", -1, -1);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.gsDumpCompression, "EmuCore/GS", "GSDumpCompression", static_cast<int>(GSDumpCompressionMethod::Uncompressed));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableFramebufferFetch, "EmuCore/GS", "DisableFramebufferFetch", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableFramebufferFetch, "EmuCore/GS", "DisableFramebufferFetch", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableDualSource, "EmuCore/GS", "DisableDualSourceBlend", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableDualSource, "EmuCore/GS", "DisableDualSourceBlend", false);

View File

@ -97,7 +97,7 @@
<string>Auto Standard (4:3/3:2 Progressive)</string> <string>Auto Standard (4:3/3:2 Progressive)</string>
</property> </property>
</item> </item>
<item> <item>
<property name="text"> <property name="text">
<string>Standard (4:3)</string> <string>Standard (4:3)</string>
</property> </property>
@ -123,7 +123,7 @@
<string>Off (Default)</string> <string>Off (Default)</string>
</property> </property>
</item> </item>
<item> <item>
<property name="text"> <property name="text">
<string>Auto Standard (4:3/3:2 Progressive)</string> <string>Auto Standard (4:3/3:2 Progressive)</string>
</property> </property>
@ -1146,7 +1146,7 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_7"> <layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="useBlitSwapChain"> <widget class="QCheckBox" name="useBlitSwapChain">
@ -1178,6 +1178,27 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0">
<widget class="QLabel" name="label_34">
<property name="text">
<string>GS Dump Compression:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="gsDumpCompression">
<item>
<property name="text">
<string>Uncompressed</string>
</property>
</item>
<item>
<property name="text">
<string>LZMA (XZ)</string>
</property>
</item>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -192,6 +192,12 @@ enum class TexturePreloadingLevel : u8
Full, Full,
}; };
enum class GSDumpCompressionMethod : u8
{
Uncompressed,
LZMA
};
// Template function for casting enumerations to their underlying type // Template function for casting enumerations to their underlying type
template <typename Enumeration> template <typename Enumeration>
typename std::underlying_type<Enumeration>::type enum_cast(Enumeration E) typename std::underlying_type<Enumeration>::type enum_cast(Enumeration E)
@ -519,6 +525,7 @@ struct Pcsx2Config
CRCHackLevel CRCHack{CRCHackLevel::Automatic}; CRCHackLevel CRCHack{CRCHackLevel::Automatic};
BiFiltering TextureFiltering{BiFiltering::PS2}; BiFiltering TextureFiltering{BiFiltering::PS2};
TexturePreloadingLevel TexturePreloading{TexturePreloadingLevel::Off}; TexturePreloadingLevel TexturePreloading{TexturePreloadingLevel::Off};
GSDumpCompressionMethod GSDumpCompression{GSDumpCompressionMethod::Uncompressed};
int Dithering{2}; int Dithering{2};
int MaxAnisotropy{0}; int MaxAnisotropy{0};
int SWExtraThreads{2}; int SWExtraThreads{2};

View File

@ -480,35 +480,6 @@ void GSvsync(u32 field, bool registers_written)
} }
} }
u32 GSmakeSnapshot(char* path)
{
try
{
std::string s{path};
if (!s.empty())
{
// Allows for providing a complete path
std::string extension = s.substr(s.size() - 4, 4);
#ifdef _WIN32
std::transform(extension.begin(), extension.end(), extension.begin(), (char(_cdecl*)(int))tolower);
#else
std::transform(extension.begin(), extension.end(), extension.begin(), tolower);
#endif
if (extension == ".png")
return g_gs_renderer->MakeSnapshot(s);
else if (s[s.length() - 1] != DIRECTORY_SEPARATOR)
s = s + DIRECTORY_SEPARATOR;
}
return g_gs_renderer->MakeSnapshot(s + "gs");
}
catch (GSRecoverableError)
{
return false;
}
}
int GSfreeze(FreezeAction mode, freezeData* data) int GSfreeze(FreezeAction mode, freezeData* data)
{ {
try try
@ -533,6 +504,18 @@ int GSfreeze(FreezeAction mode, freezeData* data)
return 0; return 0;
} }
void GSQueueSnapshot(const std::string& path, u32 gsdump_frames)
{
if (g_gs_renderer)
g_gs_renderer->QueueSnapshot(path, gsdump_frames);
}
void GSStopGSDump()
{
if (g_gs_renderer)
g_gs_renderer->StopGSDump();
}
#ifndef PCSX2_CORE #ifndef PCSX2_CORE
void GSkeyEvent(const HostKeyEvent& e) void GSkeyEvent(const HostKeyEvent& e)
@ -577,9 +560,7 @@ int GStest()
return 0; return 0;
} }
#endif static void pt(const char* str)
void pt(const char* str)
{ {
struct tm* current; struct tm* current;
time_t now; time_t now;
@ -623,6 +604,7 @@ void GSendRecording()
g_gs_renderer->EndCapture(); g_gs_renderer->EndCapture();
pt(" - Capture ended\n"); pt(" - Capture ended\n");
} }
#endif
void GSsetGameCRC(u32 crc, int options) void GSsetGameCRC(u32 crc, int options)
{ {
@ -1293,6 +1275,9 @@ void GSApp::Init()
m_gs_tv_shaders.push_back(GSSetting(3, "Triangular filter", "")); m_gs_tv_shaders.push_back(GSSetting(3, "Triangular filter", ""));
m_gs_tv_shaders.push_back(GSSetting(4, "Wave filter", "")); m_gs_tv_shaders.push_back(GSSetting(4, "Wave filter", ""));
m_gs_dump_compression.push_back(GSSetting(static_cast<u32>(GSDumpCompressionMethod::Uncompressed), "Uncompressed", ""));
m_gs_dump_compression.push_back(GSSetting(static_cast<u32>(GSDumpCompressionMethod::LZMA), "LZMA (XZ)", ""));
// clang-format off // clang-format off
// Avoid to clutter the ini file with useless options // Avoid to clutter the ini file with useless options
#if defined(ENABLE_VULKAN) || defined(_WIN32) #if defined(ENABLE_VULKAN) || defined(_WIN32)
@ -1333,6 +1318,7 @@ void GSApp::Init()
m_default_configuration["filter"] = std::to_string(static_cast<s8>(BiFiltering::PS2)); m_default_configuration["filter"] = std::to_string(static_cast<s8>(BiFiltering::PS2));
m_default_configuration["FMVSoftwareRendererSwitch"] = "0"; m_default_configuration["FMVSoftwareRendererSwitch"] = "0";
m_default_configuration["fxaa"] = "0"; m_default_configuration["fxaa"] = "0";
m_default_configuration["GSDumpCompression"] = "0";
m_default_configuration["HWDisableReadbacks"] = "0"; m_default_configuration["HWDisableReadbacks"] = "0";
m_default_configuration["pcrtc_offsets"] = "0"; m_default_configuration["pcrtc_offsets"] = "0";
m_default_configuration["IntegerScaling"] = "0"; m_default_configuration["IntegerScaling"] = "0";
@ -1581,8 +1567,32 @@ static void HotkeyAdjustZoom(double delta)
GetMTGS().RunOnGSThread([new_zoom]() { GSConfig.Zoom = new_zoom; }); GetMTGS().RunOnGSThread([new_zoom]() { GSConfig.Zoom = new_zoom; });
} }
BEGIN_HOTKEY_LIST(g_gs_hotkeys){ BEGIN_HOTKEY_LIST(g_gs_hotkeys)
"ToggleSoftwareRendering", "Graphics", "Toggle Software Rendering", [](bool pressed) { {"Screenshot", "Graphics", "Save Screenshot", [](bool pressed) {
if (!pressed)
{
GetMTGS().RunOnGSThread([]() {
GSQueueSnapshot(std::string(), 0);
});
}
}},
{"GSDumpSingleFrame", "Graphics", "Save Single Frame GS Dump", [](bool pressed) {
if (!pressed)
{
GetMTGS().RunOnGSThread([]() {
GSQueueSnapshot(std::string(), 1);
});
}
}},
{"GSDumpMultiFrame", "Graphics", "Save Multi Frame GS Dump", [](bool pressed) {
GetMTGS().RunOnGSThread([pressed]() {
if (pressed)
GSQueueSnapshot(std::string(), std::numeric_limits<u32>::max());
else
GSStopGSDump();
});
}},
{"ToggleSoftwareRendering", "Graphics", "Toggle Software Rendering", [](bool pressed) {
if (!pressed) if (!pressed)
GetMTGS().ToggleSoftwareRendering(); GetMTGS().ToggleSoftwareRendering();
}}, }},

View File

@ -66,15 +66,16 @@ void GSgifTransfer1(u8* mem, u32 addr);
void GSgifTransfer2(u8* mem, u32 size); void GSgifTransfer2(u8* mem, u32 size);
void GSgifTransfer3(u8* mem, u32 size); void GSgifTransfer3(u8* mem, u32 size);
void GSvsync(u32 field, bool registers_written); void GSvsync(u32 field, bool registers_written);
u32 GSmakeSnapshot(char* path);
int GSfreeze(FreezeAction mode, freezeData* data); int GSfreeze(FreezeAction mode, freezeData* data);
void GSQueueSnapshot(const std::string& path, u32 gsdump_frames = 0);
void GSStopGSDump();
#ifndef PCSX2_CORE #ifndef PCSX2_CORE
void GSkeyEvent(const HostKeyEvent& e); void GSkeyEvent(const HostKeyEvent& e);
void GSconfigure(); void GSconfigure();
int GStest(); int GStest();
#endif
bool GSsetupRecording(std::string& filename); bool GSsetupRecording(std::string& filename);
void GSendRecording(); void GSendRecording();
#endif
void GSsetGameCRC(u32 crc, int options); void GSsetGameCRC(u32 crc, int options);
void GSsetFrameSkip(int frameskip); void GSsetFrameSkip(int frameskip);
@ -139,6 +140,7 @@ public:
std::vector<GSSetting> m_gs_crc_level; std::vector<GSSetting> m_gs_crc_level;
std::vector<GSSetting> m_gs_acc_blend_level; std::vector<GSSetting> m_gs_acc_blend_level;
std::vector<GSSetting> m_gs_tv_shaders; std::vector<GSSetting> m_gs_tv_shaders;
std::vector<GSSetting> m_gs_dump_compression;
}; };
struct GSError struct GSError

View File

@ -17,14 +17,17 @@
#include "GSDump.h" #include "GSDump.h"
#include "GSExtra.h" #include "GSExtra.h"
#include "GSState.h" #include "GSState.h"
#include "common/Console.h"
#include "common/FileSystem.h"
GSDumpBase::GSDumpBase(const std::string& fn) GSDumpBase::GSDumpBase(std::string fn)
: m_frames(0) : m_filename(std::move(fn))
, m_frames(0)
, m_extra_frames(2) , m_extra_frames(2)
{ {
m_gs = px_fopen(fn, "wb"); m_gs = FileSystem::OpenCFile(m_filename.c_str(), "wb");
if (!m_gs) if (!m_gs)
fprintf(stderr, "GSDump: Error failed to open %s\n", fn.c_str()); Console.Error("GSDump: Error failed to open %s", m_filename.c_str());
} }
GSDumpBase::~GSDumpBase() GSDumpBase::~GSDumpBase()

View File

@ -56,9 +56,10 @@ struct GSDumpHeader
class GSDumpBase class GSDumpBase
{ {
FILE* m_gs;
std::string m_filename;
int m_frames; int m_frames;
int m_extra_frames; int m_extra_frames;
FILE* m_gs;
protected: protected:
void AddHeader(const std::string& serial, u32 crc, void AddHeader(const std::string& serial, u32 crc,
@ -70,9 +71,11 @@ protected:
virtual void AppendRawData(u8 c) = 0; virtual void AppendRawData(u8 c) = 0;
public: public:
GSDumpBase(const std::string& fn); GSDumpBase(std::string fn);
virtual ~GSDumpBase(); virtual ~GSDumpBase();
__fi const std::string& GetPath() const { return m_filename; }
void ReadFIFO(u32 size); void ReadFIFO(u32 size);
void Transfer(int index, const u8* mem, size_t size); void Transfer(int index, const u8* mem, size_t size);
bool VSync(int field, bool last, const GSPrivRegSet* regs); bool VSync(int field, bool last, const GSPrivRegSet* regs);

View File

@ -21,7 +21,9 @@
#include "PerformanceMetrics.h" #include "PerformanceMetrics.h"
#include "pcsx2/Config.h" #include "pcsx2/Config.h"
#include "common/FileSystem.h" #include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h" #include "common/StringUtil.h"
#include "fmt/core.h"
#ifndef PCSX2_CORE #ifndef PCSX2_CORE
#include "gui/AppCoreThread.h" #include "gui/AppCoreThread.h"
@ -54,17 +56,9 @@ static std::string GetDumpSerial()
std::unique_ptr<GSRenderer> g_gs_renderer; std::unique_ptr<GSRenderer> g_gs_renderer;
GSRenderer::GSRenderer() GSRenderer::GSRenderer() = default;
: m_shift_key(false)
, m_control_key(false)
, m_texture_shuffle(false)
, m_real_size(0, 0)
{
}
GSRenderer::~GSRenderer() GSRenderer::~GSRenderer() = default;
{
}
void GSRenderer::Destroy() void GSRenderer::Destroy()
{ {
@ -541,10 +535,14 @@ void GSRenderer::VSync(u32 field, bool registers_written)
g_gs_device->RestoreAPIState(); g_gs_device->RestoreAPIState();
// snapshot // snapshot
// wx is dumb and call this from the UI thread...
#ifndef PCSX2_CORE
std::unique_lock snapshot_lock(m_snapshot_mutex);
#endif
if (!m_snapshot.empty()) if (!m_snapshot.empty())
{ {
if (!m_dump && m_shift_key) if (!m_dump && m_dump_frames > 0)
{ {
freezeData fd = {0, nullptr}; freezeData fd = {0, nullptr};
Freeze(&fd, true); Freeze(&fd, true);
@ -557,12 +555,14 @@ void GSRenderer::VSync(u32 field, bool registers_written)
std::vector<u32> screenshot_pixels; std::vector<u32> screenshot_pixels;
SaveSnapshotToMemory(DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT, &screenshot_pixels); SaveSnapshotToMemory(DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT, &screenshot_pixels);
if (m_control_key) std::string_view compression_str;
if (GSConfig.GSDumpCompression == GSDumpCompressionMethod::Uncompressed)
{ {
m_dump = std::unique_ptr<GSDumpBase>(new GSDumpUncompressed(m_snapshot, GetDumpSerial(), m_crc, m_dump = std::unique_ptr<GSDumpBase>(new GSDumpUncompressed(m_snapshot, GetDumpSerial(), m_crc,
DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT, DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT,
screenshot_pixels.empty() ? nullptr : screenshot_pixels.data(), screenshot_pixels.empty() ? nullptr : screenshot_pixels.data(),
fd, m_regs)); fd, m_regs));
compression_str = "with no compression";
} }
else else
{ {
@ -570,26 +570,49 @@ void GSRenderer::VSync(u32 field, bool registers_written)
DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT, DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT,
screenshot_pixels.empty() ? nullptr : screenshot_pixels.data(), screenshot_pixels.empty() ? nullptr : screenshot_pixels.data(),
fd, m_regs)); fd, m_regs));
compression_str = "with LZMA compression";
} }
delete[] fd.data; delete[] fd.data;
Host::AddKeyedOSDMessage("GSDump", fmt::format("Saving {0} GS dump {1} to '{2}'",
(m_dump_frames == 1) ? "single frame" : "multi-frame", compression_str,
FileSystem::GetFileNameFromPath(m_dump->GetPath())), 10.0f);
} }
if (GSTexture* t = g_gs_device->GetCurrent()) if (GSTexture* t = g_gs_device->GetCurrent())
{ {
t->Save(m_snapshot + ".png"); const std::string path(m_snapshot + ".png");
const std::string_view filename(FileSystem::GetFileNameFromPath(path));
if (t->Save(path))
{
Host::AddKeyedOSDMessage("GSScreenshot",
fmt::format("Screenshot saved to '{}'.", FileSystem::GetFileNameFromPath(path)), 10.0f);
}
else
{
Host::AddFormattedOSDMessage(10.0f, "Failed to save screenshot to '%s'.", path.c_str());
}
} }
m_snapshot.clear(); m_snapshot = {};
} }
else if (m_dump) else if (m_dump)
{ {
if (m_dump->VSync(field, !m_control_key, m_regs)) const bool last = (m_dump_frames == 0);
if (m_dump->VSync(field, last, m_regs))
{
Host::AddKeyedOSDMessage("GSDump", fmt::format("Saved GS dump to '{}'.", FileSystem::GetFileNameFromPath(m_dump->GetPath())), 10.0f);
m_dump.reset(); m_dump.reset();
}
else if (!last)
{
m_dump_frames--;
}
} }
#ifndef PCSX2_CORE
// capture // capture
if (m_capture.IsCapturing()) if (m_capture.IsCapturing())
{ {
if (GSTexture* current = g_gs_device->GetCurrent()) if (GSTexture* current = g_gs_device->GetCurrent())
@ -610,55 +633,74 @@ void GSRenderer::VSync(u32 field, bool registers_written)
} }
} }
} }
#endif
} }
bool GSRenderer::MakeSnapshot(const std::string& path) void GSRenderer::QueueSnapshot(const std::string& path, u32 gsdump_frames)
{ {
if (m_snapshot.empty()) if (!m_snapshot.empty())
return;
// Allows for providing a complete path
if (path.size() > 4 && StringUtil::compareNoCase(path.substr(path.size() - 4, 4), ".png"))
{ {
// Allows for providing a complete path m_snapshot = path.substr(0, path.size() - 4);
if (path.substr(path.size() - 4, 4) == ".png") }
m_snapshot = path.substr(0, path.size() - 4); else
else {
time_t cur_time = time(nullptr);
static time_t prev_snap;
// The variable 'n' is used for labelling the screenshots when multiple screenshots are taken in
// a single second, we'll start using this variable for naming when a second screenshot request is detected
// at the same time as the first one. Hence, we're initially setting this counter to 2 to imply that
// the captured image is the 2nd image captured at this specific time.
static int n = 2;
char local_time[16];
if (strftime(local_time, sizeof(local_time), "%Y%m%d%H%M%S", localtime(&cur_time)))
{ {
time_t cur_time = time(nullptr); if (cur_time == prev_snap)
static time_t prev_snap; m_snapshot = fmt::format("gs_{0}_({1})", local_time, n++);
// The variable 'n' is used for labelling the screenshots when multiple screenshots are taken in else
// a single second, we'll start using this variable for naming when a second screenshot request is detected
// at the same time as the first one. Hence, we're initially setting this counter to 2 to imply that
// the captured image is the 2nd image captured at this specific time.
static int n = 2;
char local_time[16];
if (strftime(local_time, sizeof(local_time), "%Y%m%d%H%M%S", localtime(&cur_time)))
{ {
if (cur_time == prev_snap) n = 2;
m_snapshot = format("%s_%s_(%d)", path.c_str(), local_time, n++); m_snapshot = fmt::format("gs_{}", local_time);
else
{
n = 2;
m_snapshot = format("%s_%s", path.c_str(), local_time);
}
prev_snap = cur_time;
}
// append the game serial and title
if (std::string name(GetDumpName()); !name.empty())
{
FileSystem::SanitizeFileName(name);
m_snapshot += format("_%s", name.c_str());
}
if (std::string serial(GetDumpSerial()); !serial.empty())
{
FileSystem::SanitizeFileName(serial);
m_snapshot += format("_%s", serial.c_str());
} }
prev_snap = cur_time;
} }
// append the game serial and title
if (std::string name(GetDumpName()); !name.empty())
{
FileSystem::SanitizeFileName(name);
m_snapshot += '_';
m_snapshot += name;
}
if (std::string serial(GetDumpSerial()); !serial.empty())
{
FileSystem::SanitizeFileName(serial);
m_snapshot += '_';
m_snapshot += serial;
}
// prepend snapshots directory
m_snapshot = Path::CombineStdString(EmuFolders::Snapshots, m_snapshot);
} }
return true; // this is really gross, but wx we get the snapshot request after shift...
#ifdef PCSX2_CORE
m_dump_frames = gsdump_frames;
#endif
} }
void GSRenderer::StopGSDump()
{
m_snapshot = {};
m_dump_frames = 0;
}
#ifndef PCSX2_CORE
bool GSRenderer::BeginCapture(std::string& filename) bool GSRenderer::BeginCapture(std::string& filename)
{ {
return m_capture.BeginCapture(GetTvRefreshRate(), GetInternalResolution(), GetCurrentAspectRatioFloat(GetVideoMode() == GSVideoMode::SDTV_480P), filename); return m_capture.BeginCapture(GetTvRefreshRate(), GetInternalResolution(), GetCurrentAspectRatioFloat(GetVideoMode() == GSVideoMode::SDTV_480P), filename);
@ -671,7 +713,6 @@ void GSRenderer::EndCapture()
void GSRenderer::KeyEvent(const HostKeyEvent& e) void GSRenderer::KeyEvent(const HostKeyEvent& e)
{ {
#if !defined(PCSX2_CORE)
#ifdef _WIN32 #ifdef _WIN32
m_shift_key = !!(::GetAsyncKeyState(VK_SHIFT) & 0x8000); m_shift_key = !!(::GetAsyncKeyState(VK_SHIFT) & 0x8000);
m_control_key = !!(::GetAsyncKeyState(VK_CONTROL) & 0x8000); m_control_key = !!(::GetAsyncKeyState(VK_CONTROL) & 0x8000);
@ -696,6 +737,19 @@ void GSRenderer::KeyEvent(const HostKeyEvent& e)
} }
#endif #endif
if (m_dump_frames == 0)
{
// start dumping
if (m_shift_key)
m_dump_frames = m_control_key ? std::numeric_limits<u32>::max() : 1;
}
else
{
// stop dumping
if (m_dump && !m_control_key)
m_dump_frames = 0;
}
if (e.type == HostKeyEvent::Type::KeyPressed) if (e.type == HostKeyEvent::Type::KeyPressed)
{ {
@ -732,9 +786,10 @@ void GSRenderer::KeyEvent(const HostKeyEvent& e)
return; return;
} }
} }
#endif // PCSX2_CORE
} }
#endif // PCSX2_CORE
void GSRenderer::PurgePool() void GSRenderer::PurgePool()
{ {
g_gs_device->PurgePool(); g_gs_device->PurgePool();

View File

@ -19,21 +19,29 @@
#include "GS/GSCapture.h" #include "GS/GSCapture.h"
#include <memory> #include <memory>
#ifndef PCSX2_CORE
#include <mutex>
#endif
struct HostKeyEvent; struct HostKeyEvent;
class GSRenderer : public GSState class GSRenderer : public GSState
{ {
GSCapture m_capture; private:
std::string m_snapshot;
bool Merge(int field); bool Merge(int field);
bool m_shift_key; #ifndef PCSX2_CORE
bool m_control_key; GSCapture m_capture;
std::mutex m_snapshot_mutex;
bool m_shift_key = false;
bool m_control_key = false;
#endif
std::string m_snapshot;
u32 m_dump_frames = 0;
protected: protected:
GSVector2i m_real_size{0, 0};
bool m_texture_shuffle; bool m_texture_shuffle;
GSVector2i m_real_size;
virtual GSTexture* GetOutput(int i, int& y_offset) = 0; virtual GSTexture* GetOutput(int i, int& y_offset) = 0;
virtual GSTexture* GetFeedbackOutput() { return nullptr; } virtual GSTexture* GetFeedbackOutput() { return nullptr; }
@ -45,20 +53,24 @@ public:
virtual void Destroy(); virtual void Destroy();
virtual void VSync(u32 field, bool registers_written); virtual void VSync(u32 field, bool registers_written);
virtual bool MakeSnapshot(const std::string& path);
virtual void KeyEvent(const HostKeyEvent& e);
virtual bool CanUpscale() { return false; } virtual bool CanUpscale() { return false; }
virtual int GetUpscaleMultiplier() { return 1; } virtual int GetUpscaleMultiplier() { return 1; }
virtual GSVector2 GetTextureScaleFactor() { return { 1.0f, 1.0f }; } virtual GSVector2 GetTextureScaleFactor() { return { 1.0f, 1.0f }; }
GSVector2i GetInternalResolution(); GSVector2i GetInternalResolution();
virtual bool BeginCapture(std::string& filename);
virtual void EndCapture();
virtual void PurgePool() override; virtual void PurgePool() override;
virtual void PurgeTextureCache(); virtual void PurgeTextureCache();
bool SaveSnapshotToMemory(u32 width, u32 height, std::vector<u32>* pixels); bool SaveSnapshotToMemory(u32 width, u32 height, std::vector<u32>* pixels);
void QueueSnapshot(const std::string& path, u32 gsdump_frames);
void StopGSDump();
#ifndef PCSX2_CORE
bool BeginCapture(std::string& filename);
void EndCapture();
void KeyEvent(const HostKeyEvent& e);
#endif
}; };
extern std::unique_ptr<GSRenderer> g_gs_renderer; extern std::unique_ptr<GSRenderer> g_gs_renderer;

View File

@ -586,6 +586,7 @@ DebugTab::DebugTab(wxWindow* parent)
m_ui.addComboBoxAndLabel(ogl_grid, "Geometry Shader:", "OverrideGeometryShaders", &theApp.m_gs_generic_list, IDC_GEOMETRY_SHADER_OVERRIDE, vk_ogl_hw_prereq); m_ui.addComboBoxAndLabel(ogl_grid, "Geometry Shader:", "OverrideGeometryShaders", &theApp.m_gs_generic_list, IDC_GEOMETRY_SHADER_OVERRIDE, vk_ogl_hw_prereq);
m_ui.addComboBoxAndLabel(ogl_grid, "Image Load Store:", "override_GL_ARB_shader_image_load_store", &theApp.m_gs_generic_list, IDC_IMAGE_LOAD_STORE, ogl_hw_prereq); m_ui.addComboBoxAndLabel(ogl_grid, "Image Load Store:", "override_GL_ARB_shader_image_load_store", &theApp.m_gs_generic_list, IDC_IMAGE_LOAD_STORE, ogl_hw_prereq);
m_ui.addComboBoxAndLabel(ogl_grid, "Sparse Texture:", "override_GL_ARB_sparse_texture", &theApp.m_gs_generic_list, IDC_SPARSE_TEXTURE, ogl_hw_prereq); m_ui.addComboBoxAndLabel(ogl_grid, "Sparse Texture:", "override_GL_ARB_sparse_texture", &theApp.m_gs_generic_list, IDC_SPARSE_TEXTURE, ogl_hw_prereq);
m_ui.addComboBoxAndLabel(ogl_grid, "Dump Compression:", "GSDumpCompression", &theApp.m_gs_dump_compression, -1);
ogl_box->Add(ogl_grid); ogl_box->Add(ogl_grid);
tab_box->Add(ogl_box.outer, wxSizerFlags().Expand()); tab_box->Add(ogl_box.outer, wxSizerFlags().Expand());

View File

@ -391,6 +391,7 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const
OpEqu(CRCHack) && OpEqu(CRCHack) &&
OpEqu(TextureFiltering) && OpEqu(TextureFiltering) &&
OpEqu(TexturePreloading) && OpEqu(TexturePreloading) &&
OpEqu(GSDumpCompression) &&
OpEqu(Dithering) && OpEqu(Dithering) &&
OpEqu(MaxAnisotropy) && OpEqu(MaxAnisotropy) &&
OpEqu(SWExtraThreads) && OpEqu(SWExtraThreads) &&
@ -575,6 +576,7 @@ void Pcsx2Config::GSOptions::ReloadIniSettings()
GSSettingIntEnumEx(CRCHack, "crc_hack_level"); GSSettingIntEnumEx(CRCHack, "crc_hack_level");
GSSettingIntEnumEx(TextureFiltering, "filter"); GSSettingIntEnumEx(TextureFiltering, "filter");
GSSettingIntEnumEx(TexturePreloading, "texture_preloading"); GSSettingIntEnumEx(TexturePreloading, "texture_preloading");
GSSettingIntEnumEx(GSDumpCompression, "GSDumpCompression");
GSSettingIntEx(Dithering, "dithering_ps2"); GSSettingIntEx(Dithering, "dithering_ps2");
GSSettingIntEx(MaxAnisotropy, "MaxAnisotropy"); GSSettingIntEx(MaxAnisotropy, "MaxAnisotropy");
GSSettingIntEx(SWExtraThreads, "extrathreads"); GSSettingIntEx(SWExtraThreads, "extrathreads");

View File

@ -372,8 +372,7 @@ namespace Implementations
void Sys_TakeSnapshot() void Sys_TakeSnapshot()
{ {
if (GSmakeSnapshot(g_Conf->Folders.Snapshots.ToUTF8().data())) GSQueueSnapshot(std::string(), 0);
OSDlog(ConsoleColors::Color_Black, true, "Snapshot taken");
} }
void Sys_RenderToggle() void Sys_RenderToggle()

View File

@ -1040,7 +1040,7 @@ void MainEmuFrame::Menu_Capture_Screenshot_Screenshot_Click(wxCommandEvent& even
{ {
return; return;
} }
GSmakeSnapshot(g_Conf->Folders.Snapshots.ToString().char_str()); GSQueueSnapshot(std::string(), 0);
} }
void MainEmuFrame::Menu_Capture_Screenshot_Screenshot_As_Click(wxCommandEvent& event) void MainEmuFrame::Menu_Capture_Screenshot_Screenshot_As_Click(wxCommandEvent& event)
@ -1056,7 +1056,7 @@ void MainEmuFrame::Menu_Capture_Screenshot_Screenshot_As_Click(wxCommandEvent& e
wxFileDialog fileDialog(this, _("Select a file"), g_Conf->Folders.Snapshots.ToAscii(), wxEmptyString, "PNG files (*.png)|*.png", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); wxFileDialog fileDialog(this, _("Select a file"), g_Conf->Folders.Snapshots.ToAscii(), wxEmptyString, "PNG files (*.png)|*.png", wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (fileDialog.ShowModal() == wxID_OK) if (fileDialog.ShowModal() == wxID_OK)
GSmakeSnapshot((char*)fileDialog.GetPath().char_str()); GSQueueSnapshot(StringUtil::wxStringToUTF8String(fileDialog.GetPath()), 0);
// Resume emulation // Resume emulation
if (!wasPaused) if (!wasPaused)