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