diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp
index 44c70d37ad..8c3f398356 100644
--- a/pcsx2-qt/MainWindow.cpp
+++ b/pcsx2-qt/MainWindow.cpp
@@ -46,7 +46,7 @@
#include "SettingWidgetBinder.h"
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 *.gs.zst);;"
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
"MAME CHD Images (*.chd);;"
@@ -55,7 +55,7 @@ static constexpr char DISC_IMAGE_FILTER[] =
"ELF Executables (*.elf);;"
"IRX Executables (*.irx);;"
"Playlists (*.m3u);;"
- "GS Dumps (*.gs *.gs.xz)");
+ "GS Dumps (*.gs *.gs.xz *.gs.zst)");
const char* MainWindow::DEFAULT_THEME_NAME = "darkfusion";
diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui
index f3bccb92ff..1437de9935 100644
--- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui
+++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui
@@ -1194,7 +1194,12 @@
-
- LZMA (XZ)
+ LZMA (xz)
+
+
+ -
+
+ Zstandard (zst)
diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt
index 9dc3d3f829..12bbcdfdda 100644
--- a/pcsx2/CMakeLists.txt
+++ b/pcsx2/CMakeLists.txt
@@ -1604,6 +1604,7 @@ target_link_libraries(PCSX2_FLAGS INTERFACE
PkgConfig::SAMPLERATE
PNG::PNG
LibLZMA::LibLZMA
+ Zstd::Zstd
${LIBC_LIBRARIES}
)
diff --git a/pcsx2/Config.h b/pcsx2/Config.h
index 1af2454754..5a72ac233d 100644
--- a/pcsx2/Config.h
+++ b/pcsx2/Config.h
@@ -195,7 +195,8 @@ enum class TexturePreloadingLevel : u8
enum class GSDumpCompressionMethod : u8
{
Uncompressed,
- LZMA
+ LZMA,
+ Zstandard,
};
// Template function for casting enumerations to their underlying type
diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp
index c3195263f5..06a14e0487 100644
--- a/pcsx2/GS/GS.cpp
+++ b/pcsx2/GS/GS.cpp
@@ -1276,7 +1276,8 @@ void GSApp::Init()
m_gs_tv_shaders.push_back(GSSetting(4, "Wave filter", ""));
m_gs_dump_compression.push_back(GSSetting(static_cast(GSDumpCompressionMethod::Uncompressed), "Uncompressed", ""));
- m_gs_dump_compression.push_back(GSSetting(static_cast(GSDumpCompressionMethod::LZMA), "LZMA (XZ)", ""));
+ m_gs_dump_compression.push_back(GSSetting(static_cast(GSDumpCompressionMethod::LZMA), "LZMA (xz)", ""));
+ m_gs_dump_compression.push_back(GSSetting(static_cast(GSDumpCompressionMethod::Zstandard), "Zstandard (zst)", ""));
// clang-format off
// Avoid to clutter the ini file with useless options
diff --git a/pcsx2/GS/GSDump.cpp b/pcsx2/GS/GSDump.cpp
index d1ae13e974..56fd0e984c 100644
--- a/pcsx2/GS/GSDump.cpp
+++ b/pcsx2/GS/GSDump.cpp
@@ -226,3 +226,92 @@ void GSDumpXz::Compress(lzma_action action, lzma_ret expected_status)
} while (m_strm.avail_out == 0);
}
+
+//////////////////////////////////////////////////////////////////////
+// GSDumpZstd implementation
+//////////////////////////////////////////////////////////////////////
+
+GSDumpZst::GSDumpZst(const std::string& fn, const std::string& serial, u32 crc,
+ u32 screenshot_width, u32 screenshot_height, const u32* screenshot_pixels,
+ const freezeData& fd, const GSPrivRegSet* regs)
+ : GSDumpBase(fn + ".gs.zst")
+{
+ m_strm = ZSTD_createCStream();
+
+ // Compression level 6 provides a good balance between speed and ratio.
+ ZSTD_CCtx_setParameter(m_strm, ZSTD_c_compressionLevel, 6);
+
+ m_in_buff.reserve(_1mb);
+ m_out_buff.resize(_1mb);
+
+ AddHeader(serial, crc, screenshot_width, screenshot_height, screenshot_pixels, fd, regs);
+}
+
+GSDumpZst::~GSDumpZst()
+{
+ // Finish the stream
+ Compress(ZSTD_e_end);
+
+ ZSTD_freeCStream(m_strm);
+}
+
+void GSDumpZst::AppendRawData(const void* data, size_t size)
+{
+ size_t old_size = m_in_buff.size();
+ m_in_buff.resize(old_size + size);
+ memcpy(&m_in_buff[old_size], data, size);
+ MayFlush();
+}
+
+void GSDumpZst::AppendRawData(u8 c)
+{
+ m_in_buff.push_back(c);
+ MayFlush();
+}
+
+void GSDumpZst::MayFlush()
+{
+ if (m_in_buff.size() >= _1mb)
+ Compress(ZSTD_e_continue);
+}
+
+void GSDumpZst::Compress(ZSTD_EndDirective action)
+{
+ if (m_in_buff.empty())
+ return;
+
+ ZSTD_inBuffer inbuf = {m_in_buff.data(), m_in_buff.size(), 0};
+
+ for (;;)
+ {
+ ZSTD_outBuffer outbuf = {m_out_buff.data(), m_out_buff.size(), 0};
+
+ const size_t remaining = ZSTD_compressStream2(m_strm, &outbuf, &inbuf, action);
+ if (ZSTD_isError(remaining))
+ {
+ fprintf(stderr, "GSDumpZstd: Error %s\n", ZSTD_getErrorName(remaining));
+ return;
+ }
+
+ if (outbuf.pos > 0)
+ {
+ Write(m_out_buff.data(), outbuf.pos);
+ outbuf.pos = 0;
+ }
+
+ if (action == ZSTD_e_end)
+ {
+ // break when compression output has finished
+ if (remaining == 0)
+ break;
+ }
+ else
+ {
+ // break when all input data is consumed
+ if (inbuf.pos == inbuf.size)
+ break;
+ }
+ }
+
+ m_in_buff.clear();
+}
diff --git a/pcsx2/GS/GSDump.h b/pcsx2/GS/GSDump.h
index b7ba79ccbf..f269d140e4 100644
--- a/pcsx2/GS/GSDump.h
+++ b/pcsx2/GS/GSDump.h
@@ -19,6 +19,7 @@
#include "GSRegs.h"
#include "Renderers/SW/GSVertexSW.h"
#include
+#include
/*
@@ -110,3 +111,22 @@ public:
const freezeData& fd, const GSPrivRegSet* regs);
virtual ~GSDumpXz();
};
+
+class GSDumpZst final : public GSDumpBase
+{
+ ZSTD_CStream* m_strm;
+
+ std::vector m_in_buff;
+ std::vector m_out_buff;
+
+ void MayFlush();
+ void Compress(ZSTD_EndDirective action);
+ void AppendRawData(const void* data, size_t size);
+ void AppendRawData(u8 c);
+
+public:
+ GSDumpZst(const std::string& fn, const std::string& serial, u32 crc,
+ u32 screenshot_width, u32 screenshot_height, const u32* screenshot_pixels,
+ const freezeData& fd, const GSPrivRegSet* regs);
+ virtual ~GSDumpZst();
+};
diff --git a/pcsx2/GS/GSLzma.cpp b/pcsx2/GS/GSLzma.cpp
index 9dc2c2ef14..67f311f758 100644
--- a/pcsx2/GS/GSLzma.cpp
+++ b/pcsx2/GS/GSLzma.cpp
@@ -66,6 +66,8 @@ std::unique_ptr GSDumpFile::OpenGSDump(const char* filename, const c
if (StringUtil::EndsWithNoCase(filename, ".xz"))
return std::make_unique(fp, nullptr);
+ else if (StringUtil::EndsWithNoCase(filename, ".zst"))
+ return std::make_unique(fp, nullptr);
else
return std::make_unique(fp, nullptr);
}
@@ -359,6 +361,95 @@ GSDumpLzma::~GSDumpLzma()
_aligned_free(m_area);
}
+/******************************************************************/
+GSDumpDecompressZst::GSDumpDecompressZst(FILE* file, FILE* repack_file)
+ : GSDumpFile(file, repack_file)
+{
+ Initialize();
+}
+
+void GSDumpDecompressZst::Initialize()
+{
+ m_strm = ZSTD_createDStream();
+
+ m_area = (uint8_t*)_aligned_malloc(OUTPUT_BUFFER_SIZE, 32);
+ m_inbuf.src = (uint8_t*)_aligned_malloc(INPUT_BUFFER_SIZE, 32);
+ m_inbuf.pos = 0;
+ m_inbuf.size = 0;
+ m_avail = 0;
+ m_start = 0;
+}
+
+void GSDumpDecompressZst::Decompress()
+{
+ ZSTD_outBuffer outbuf = { m_area, OUTPUT_BUFFER_SIZE, 0 };
+ while (outbuf.pos == 0)
+ {
+ // Nothing left in the input buffer. Read data from the file
+ if (m_inbuf.pos == m_inbuf.size && !feof(m_fp))
+ {
+ m_inbuf.size = fread((void*)m_inbuf.src, 1, INPUT_BUFFER_SIZE, m_fp);
+ m_inbuf.pos = 0;
+
+ if (ferror(m_fp))
+ {
+ fprintf(stderr, "Read error: %s\n", strerror(errno));
+ throw "BAD"; // Just exit the program
+ }
+ }
+
+ size_t ret = ZSTD_decompressStream(m_strm, &outbuf, &m_inbuf);
+ if (ZSTD_isError(ret))
+ {
+ fprintf(stderr, "Decoder error: (error code %s)\n", ZSTD_getErrorName(ret));
+ throw "BAD"; // Just exit the program
+ }
+ }
+
+ m_start = 0;
+ m_avail = outbuf.pos;
+}
+
+bool GSDumpDecompressZst::IsEof()
+{
+ return feof(m_fp) && m_avail == 0 && m_inbuf.pos == m_inbuf.size;
+}
+
+size_t GSDumpDecompressZst::Read(void* ptr, size_t size)
+{
+ size_t off = 0;
+ uint8_t* dst = (uint8_t*)ptr;
+ while (size && !IsEof())
+ {
+ if (m_avail == 0)
+ {
+ Decompress();
+ }
+
+ size_t l = std::min(size, m_avail);
+ memcpy(dst + off, m_area + m_start, l);
+ m_avail -= l;
+ size -= l;
+ m_start += l;
+ off += l;
+ }
+
+ if (off > 0)
+ Repack(ptr, off);
+
+ return off;
+}
+
+GSDumpDecompressZst::~GSDumpDecompressZst()
+{
+ ZSTD_freeDStream(m_strm);
+
+ if (m_inbuf.src)
+ _aligned_free((void*)m_inbuf.src);
+ if (m_area)
+ _aligned_free(m_area);
+}
+
/******************************************************************/
GSDumpRaw::GSDumpRaw(FILE* file, FILE* repack_file)
diff --git a/pcsx2/GS/GSLzma.h b/pcsx2/GS/GSLzma.h
index 99f79884e9..90ff6bd112 100644
--- a/pcsx2/GS/GSLzma.h
+++ b/pcsx2/GS/GSLzma.h
@@ -15,11 +15,13 @@
#pragma once
-#include
#include
#include
#include
+#include
+#include
+
#define GEN_REG_ENUM_CLASS_CONTENT(ClassName, EntryName, Value) \
EntryName = Value,
@@ -358,6 +360,30 @@ public:
size_t Read(void* ptr, size_t size) final;
};
+class GSDumpDecompressZst : public GSDumpFile
+{
+ static constexpr u32 INPUT_BUFFER_SIZE = 512 * _1kb;
+ static constexpr u32 OUTPUT_BUFFER_SIZE = 2 * _1mb;
+
+ ZSTD_DStream* m_strm;
+ ZSTD_inBuffer m_inbuf;
+
+ uint8_t* m_area;
+
+ size_t m_avail;
+ size_t m_start;
+
+ void Decompress();
+ void Initialize();
+
+public:
+ GSDumpDecompressZst(FILE* file, FILE* repack_file);
+ virtual ~GSDumpDecompressZst();
+
+ bool IsEof() final;
+ size_t Read(void* ptr, size_t size) final;
+};
+
class GSDumpRaw : public GSDumpFile
{
public:
diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp
index d10b42ffc8..ba1c36d62e 100644
--- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp
+++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp
@@ -564,7 +564,7 @@ void GSRenderer::VSync(u32 field, bool registers_written)
fd, m_regs));
compression_str = "with no compression";
}
- else
+ else if (GSConfig.GSDumpCompression == GSDumpCompressionMethod::LZMA)
{
m_dump = std::unique_ptr(new GSDumpXz(m_snapshot, GetDumpSerial(), m_crc,
DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT,
@@ -572,6 +572,14 @@ void GSRenderer::VSync(u32 field, bool registers_written)
fd, m_regs));
compression_str = "with LZMA compression";
}
+ else
+ {
+ m_dump = std::unique_ptr(new GSDumpZst(m_snapshot, GetDumpSerial(), m_crc,
+ DUMP_SCREENSHOT_WIDTH, DUMP_SCREENSHOT_HEIGHT,
+ screenshot_pixels.empty() ? nullptr : screenshot_pixels.data(),
+ fd, m_regs));
+ compression_str = "with Zstandard compression";
+ }
delete[] fd.data;
@@ -642,7 +650,7 @@ void GSRenderer::QueueSnapshot(const std::string& path, u32 gsdump_frames)
return;
// Allows for providing a complete path
- if (path.size() > 4 && StringUtil::compareNoCase(path.substr(path.size() - 4, 4), ".png"))
+ if (path.size() > 4 && StringUtil::EndsWithNoCase(path, ".png"))
{
m_snapshot = path.substr(0, path.size() - 4);
}
diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp
index 3bbc564741..d8164cc256 100644
--- a/pcsx2/VMManager.cpp
+++ b/pcsx2/VMManager.cpp
@@ -1111,7 +1111,9 @@ bool VMManager::IsElfFileName(const std::string& path)
bool VMManager::IsGSDumpFileName(const std::string& path)
{
- return (StringUtil::EndsWithNoCase(path, ".gs") || StringUtil::EndsWithNoCase(path, ".gs.xz"));
+ return (StringUtil::EndsWithNoCase(path, ".gs") ||
+ StringUtil::EndsWithNoCase(path, ".gs.xz") ||
+ StringUtil::EndsWithNoCase(path, ".gs.zst"));
}
void VMManager::Execute()
diff --git a/pcsx2/gui/Dialogs/GSDumpDialog.cpp b/pcsx2/gui/Dialogs/GSDumpDialog.cpp
index e83c524bd4..721c5064e0 100644
--- a/pcsx2/gui/Dialogs/GSDumpDialog.cpp
+++ b/pcsx2/gui/Dialogs/GSDumpDialog.cpp
@@ -222,6 +222,8 @@ void Dialogs::GSDumpDialog::GetDumpsList()
dumps.push_back(filename.substr(0, filename.length() - 3));
else if (filename.EndsWith(".gs.xz"))
dumps.push_back(filename.substr(0, filename.length() - 6));
+ else if (filename.EndsWith(".gs.zst"))
+ dumps.push_back(filename.substr(0, filename.length() - 7));
cont = snaps.GetNext(&filename);
}
std::sort(dumps.begin(), dumps.end(), [](const wxString& a, const wxString& b) { return a.CmpNoCase(b) < 0; });
@@ -249,6 +251,8 @@ void Dialogs::GSDumpDialog::SelectedDump(wxListEvent& evt)
wxString filename = g_Conf->Folders.Snapshots.ToAscii() + ("/" + evt.GetText()) + ".gs";
if (!wxFileExists(filename))
filename.append(".xz");
+ if (!wxFileExists(filename))
+ filename = filename.RemoveLast(3).append(".zst");
if (wxFileExists(filename_preview))
{
auto img = wxImage(filename_preview);
diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj
index 4262cb0fbc..91756d8f3d 100644
--- a/pcsx2/pcsx2.vcxproj
+++ b/pcsx2/pcsx2.vcxproj
@@ -44,6 +44,7 @@
$(SolutionDir)3rdparty\imgui\imgui;$(SolutionDir)3rdparty\imgui\include;%(AdditionalIncludeDirectories)
$(SolutionDir)3rdparty\libzip;$(SolutionDir)3rdparty\libzip\libzip\lib;%(AdditionalIncludeDirectories)
$(SolutionDir)3rdparty\d3d12memalloc\include;%(AdditionalIncludeDirectories)
+ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zstd\zstd\lib
Async
Use
PrecompiledHeader.h
diff --git a/pcsx2/pcsx2core.vcxproj b/pcsx2/pcsx2core.vcxproj
index 57a1f3b0c1..81cb90c86a 100644
--- a/pcsx2/pcsx2core.vcxproj
+++ b/pcsx2/pcsx2core.vcxproj
@@ -47,6 +47,7 @@
$(SolutionDir)3rdparty\sdl2\include;$(SolutionDir)3rdparty\sdl2\SDL\include;%(AdditionalIncludeDirectories)
$(SolutionDir)3rdparty\libzip;$(SolutionDir)3rdparty\libzip\libzip\lib;%(AdditionalIncludeDirectories)
$(SolutionDir)3rdparty\d3d12memalloc\include;%(AdditionalIncludeDirectories)
+ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zstd\zstd\lib
Async
Use
PrecompiledHeader.h