mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
428 lines
11 KiB
C++
428 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "GS/GSDump.h"
|
|
#include "GS/GSExtra.h"
|
|
#include "GS/GSLzma.h"
|
|
#include "GS/GSState.h"
|
|
|
|
#include "common/Console.h"
|
|
#include "common/FileSystem.h"
|
|
#include "common/HeapArray.h"
|
|
#include "common/ScopedGuard.h"
|
|
|
|
#include <7zCrc.h>
|
|
#include <XzCrc64.h>
|
|
#include <XzEnc.h>
|
|
#include <zstd.h>
|
|
|
|
GSDumpBase::GSDumpBase(std::string fn)
|
|
: m_filename(std::move(fn))
|
|
, m_frames(0)
|
|
, m_extra_frames(2)
|
|
{
|
|
m_gs = FileSystem::OpenCFile(m_filename.c_str(), "wb");
|
|
if (!m_gs)
|
|
Console.ErrorFmt("GSDump: Error failed to open {}", m_filename);
|
|
}
|
|
|
|
GSDumpBase::~GSDumpBase()
|
|
{
|
|
if (m_gs)
|
|
std::fclose(m_gs);
|
|
}
|
|
|
|
void GSDumpBase::AddHeader(const std::string& serial, u32 crc,
|
|
u32 screenshot_width, u32 screenshot_height, const u32* screenshot_pixels,
|
|
const freezeData& fd, const GSPrivRegSet* regs)
|
|
{
|
|
// New header: CRC of FFFFFFFF, secondary header, full header follows.
|
|
const u32 fake_crc = 0xFFFFFFFFu;
|
|
AppendRawData(&fake_crc, 4);
|
|
|
|
// Compute full header size (with serial).
|
|
// This acts as the state size for loading older dumps.
|
|
const u32 screenshot_size = screenshot_width * screenshot_height * sizeof(screenshot_pixels[0]);
|
|
const u32 header_size = sizeof(GSDumpHeader) + static_cast<u32>(serial.size()) + screenshot_size;
|
|
AppendRawData(&header_size, 4);
|
|
|
|
// Write hader.
|
|
GSDumpHeader header = {};
|
|
header.state_version = GSState::STATE_VERSION;
|
|
header.state_size = fd.size;
|
|
header.crc = crc;
|
|
header.serial_offset = sizeof(header);
|
|
header.serial_size = static_cast<u32>(serial.size());
|
|
header.screenshot_width = screenshot_width;
|
|
header.screenshot_height = screenshot_height;
|
|
header.screenshot_offset = header.serial_offset + header.serial_size;
|
|
header.screenshot_size = screenshot_size;
|
|
AppendRawData(&header, sizeof(header));
|
|
if (!serial.empty())
|
|
AppendRawData(serial.data(), serial.size());
|
|
if (screenshot_pixels)
|
|
AppendRawData(screenshot_pixels, screenshot_size);
|
|
|
|
// Then the real state data.
|
|
AppendRawData(fd.data, fd.size);
|
|
AppendRawData(regs, sizeof(*regs));
|
|
}
|
|
|
|
void GSDumpBase::Transfer(int index, const u8* mem, size_t size)
|
|
{
|
|
if (size == 0)
|
|
return;
|
|
|
|
AppendRawData(0);
|
|
AppendRawData(static_cast<u8>(index));
|
|
AppendRawData(&size, 4);
|
|
AppendRawData(mem, size);
|
|
}
|
|
|
|
void GSDumpBase::ReadFIFO(u32 size)
|
|
{
|
|
if (size == 0)
|
|
return;
|
|
|
|
AppendRawData(2);
|
|
AppendRawData(&size, 4);
|
|
}
|
|
|
|
bool GSDumpBase::VSync(int field, bool last, const GSPrivRegSet* regs)
|
|
{
|
|
// dump file is bad, return done to delete the object
|
|
if (!m_gs)
|
|
return true;
|
|
|
|
AppendRawData(3);
|
|
AppendRawData(regs, sizeof(*regs));
|
|
|
|
AppendRawData(1);
|
|
AppendRawData(static_cast<u8>(field));
|
|
|
|
if (last)
|
|
m_extra_frames--;
|
|
|
|
return (++m_frames & 1) == 0 && last && (m_extra_frames < 0);
|
|
}
|
|
|
|
void GSDumpBase::Write(const void* data, size_t size)
|
|
{
|
|
if (!m_gs || size == 0)
|
|
return;
|
|
|
|
size_t written = fwrite(data, 1, size, m_gs);
|
|
if (written != size)
|
|
Console.Error("GSDump: Error failed to write data");
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// GSDump implementation
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
namespace
|
|
{
|
|
class GSDumpUncompressed final : public GSDumpBase
|
|
{
|
|
void AppendRawData(const void* data, size_t size) final;
|
|
void AppendRawData(u8 c) final;
|
|
|
|
public:
|
|
GSDumpUncompressed(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 ~GSDumpUncompressed() = default;
|
|
};
|
|
|
|
GSDumpUncompressed::GSDumpUncompressed(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")
|
|
{
|
|
AddHeader(serial, crc, screenshot_width, screenshot_height, screenshot_pixels, fd, regs);
|
|
}
|
|
|
|
void GSDumpUncompressed::AppendRawData(const void* data, size_t size)
|
|
{
|
|
Write(data, size);
|
|
}
|
|
|
|
void GSDumpUncompressed::AppendRawData(u8 c)
|
|
{
|
|
Write(&c, 1);
|
|
}
|
|
} // namespace
|
|
|
|
std::unique_ptr<GSDumpBase> GSDumpBase::CreateUncompressedDump(
|
|
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)
|
|
{
|
|
return std::make_unique<GSDumpUncompressed>(fn, serial, crc,
|
|
screenshot_width, screenshot_height, screenshot_pixels,
|
|
fd, regs);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
class GsDumpBuffered : public GSDumpBase
|
|
{
|
|
protected:
|
|
void AppendRawData(const void* data, size_t size) override final;
|
|
void AppendRawData(u8 c) override final;
|
|
|
|
void EnsureSpace(size_t size);
|
|
|
|
DynamicHeapArray<u8, 64> m_buffer;
|
|
size_t m_buffer_size = 0;
|
|
|
|
public:
|
|
GsDumpBuffered(std::string fn);
|
|
virtual ~GsDumpBuffered() override = default;
|
|
};
|
|
|
|
GsDumpBuffered::GsDumpBuffered(std::string fn)
|
|
: GSDumpBase(std::move(fn))
|
|
{
|
|
m_buffer.resize(_1mb);
|
|
}
|
|
|
|
void GsDumpBuffered::AppendRawData(const void* data, size_t size)
|
|
{
|
|
if (size == 0) [[unlikely]]
|
|
return;
|
|
|
|
EnsureSpace(size);
|
|
std::memcpy(&m_buffer[m_buffer_size], data, size);
|
|
m_buffer_size += size;
|
|
}
|
|
|
|
void GsDumpBuffered::AppendRawData(u8 c)
|
|
{
|
|
EnsureSpace(1);
|
|
m_buffer[m_buffer_size++] = c;
|
|
}
|
|
|
|
void GsDumpBuffered::EnsureSpace(size_t size)
|
|
{
|
|
const size_t new_size = m_buffer_size + size;
|
|
if (new_size <= m_buffer.size())
|
|
return;
|
|
|
|
const size_t alloc_size = std::max(m_buffer.size() * 2, new_size);
|
|
m_buffer.resize(alloc_size);
|
|
}
|
|
} // namespace
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// GSDumpXz implementation
|
|
//////////////////////////////////////////////////////////////////////
|
|
namespace
|
|
{
|
|
class GSDumpXz final : public GsDumpBuffered
|
|
{
|
|
void Compress();
|
|
|
|
public:
|
|
GSDumpXz(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);
|
|
~GSDumpXz() override;
|
|
};
|
|
|
|
GSDumpXz::GSDumpXz(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)
|
|
: GsDumpBuffered(fn + ".gs.xz")
|
|
{
|
|
AddHeader(serial, crc, screenshot_width, screenshot_height, screenshot_pixels, fd, regs);
|
|
}
|
|
|
|
GSDumpXz::~GSDumpXz()
|
|
{
|
|
Compress();
|
|
}
|
|
|
|
void GSDumpXz::Compress()
|
|
{
|
|
struct MemoryInStream
|
|
{
|
|
ISeqInStream vt;
|
|
const u8* buffer;
|
|
size_t buffer_size;
|
|
size_t read_pos;
|
|
};
|
|
MemoryInStream mis = {
|
|
{.Read = [](const ISeqInStream* p, void* buf, size_t* size) -> SRes {
|
|
MemoryInStream* mis = Z7_CONTAINER_FROM_VTBL(p, MemoryInStream, vt);
|
|
const size_t avail = mis->buffer_size - mis->read_pos;
|
|
const size_t copy = std::min(avail, *size);
|
|
|
|
std::memcpy(buf, &mis->buffer[mis->read_pos], copy);
|
|
mis->read_pos += copy;
|
|
*size = copy;
|
|
return SZ_OK;
|
|
}},
|
|
m_buffer.data(),
|
|
m_buffer_size,
|
|
0};
|
|
|
|
struct DumpOutStream
|
|
{
|
|
ISeqOutStream vt;
|
|
GSDumpXz* real;
|
|
};
|
|
DumpOutStream dos = {
|
|
{.Write = [](const ISeqOutStream* p, const void* buf, size_t size) -> size_t {
|
|
DumpOutStream* dos = Z7_CONTAINER_FROM_VTBL(p, DumpOutStream, vt);
|
|
dos->real->Write(buf, size);
|
|
return size;
|
|
}},
|
|
this};
|
|
|
|
pxAssert(m_buffer_size > 0);
|
|
|
|
GSInit7ZCRCTables();
|
|
|
|
CXzProps props;
|
|
XzProps_Init(&props);
|
|
const SRes res = Xz_Encode(&dos.vt, &mis.vt, &props, nullptr);
|
|
if (res != SZ_OK)
|
|
{
|
|
Console.ErrorFmt("Xz_Encode() failed: {}", static_cast<int>(res));
|
|
return;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
std::unique_ptr<GSDumpBase> GSDumpBase::CreateXzDump(
|
|
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)
|
|
{
|
|
return std::make_unique<GSDumpXz>(fn, serial, crc,
|
|
screenshot_width, screenshot_height, screenshot_pixels,
|
|
fd, regs);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// GSDumpZstd implementation
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
namespace
|
|
{
|
|
class GSDumpZst final : public GSDumpBase
|
|
{
|
|
ZSTD_CStream* m_strm;
|
|
|
|
std::vector<u8> m_in_buff;
|
|
std::vector<u8> 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();
|
|
};
|
|
|
|
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))
|
|
{
|
|
Console.ErrorFmt("GSDumpZstd: Error {}", 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();
|
|
}
|
|
} // namespace
|
|
|
|
std::unique_ptr<GSDumpBase> GSDumpBase::CreateZstDump(
|
|
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)
|
|
{
|
|
return std::make_unique<GSDumpZst>(fn, serial, crc,
|
|
screenshot_width, screenshot_height, screenshot_pixels,
|
|
fd, regs);
|
|
}
|