mirror of
https://github.com/PCSX2/pcsx2.git
synced 2026-01-31 01:15:24 +01:00
642 lines
17 KiB
C++
642 lines
17 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "common/AlignedMalloc.h"
|
|
#include "common/Console.h"
|
|
#include "common/ScopedGuard.h"
|
|
#include "common/StringUtil.h"
|
|
#include "common/BitUtils.h"
|
|
#include "common/Error.h"
|
|
#include "common/HeapArray.h"
|
|
|
|
#include "GS/GSDump.h"
|
|
#include "GS/GSLzma.h"
|
|
#include "GS/GSExtra.h"
|
|
|
|
#include "Host.h"
|
|
|
|
#include <Alloc.h>
|
|
#include <7zCrc.h>
|
|
#include <Xz.h>
|
|
#include <XzCrc64.h>
|
|
#include <zstd.h>
|
|
|
|
#include <mutex>
|
|
|
|
using namespace GSDumpTypes;
|
|
|
|
GSDumpFile::GSDumpFile() = default;
|
|
|
|
GSDumpFile::~GSDumpFile() = default;
|
|
|
|
bool GSDumpFile::GetPreviewImageFromDump(const char* filename, u32* width, u32* height, std::vector<u32>* pixels)
|
|
{
|
|
std::unique_ptr<GSDumpFile> dump = OpenGSDump(filename);
|
|
if (!dump)
|
|
return false;
|
|
|
|
u32 crc;
|
|
if (!dump->Read(&crc, sizeof(crc)) || crc != 0xFFFFFFFFu)
|
|
{
|
|
// not new header dump, so no preview
|
|
return false;
|
|
}
|
|
|
|
u32 header_size;
|
|
if (!dump->Read(&header_size, sizeof(header_size)) || header_size < sizeof(GSDumpHeader))
|
|
{
|
|
// doesn't have the screenshot fields
|
|
return false;
|
|
}
|
|
|
|
std::unique_ptr<u8[]> header_bits = std::make_unique_for_overwrite<u8[]>(header_size);
|
|
if (!dump->Read(header_bits.get(), header_size))
|
|
return false;
|
|
|
|
GSDumpHeader header;
|
|
std::memcpy(&header, header_bits.get(), sizeof(header));
|
|
if (header.screenshot_size == 0 ||
|
|
header.screenshot_size < (header.screenshot_width * header.screenshot_height * sizeof(u32)) ||
|
|
(static_cast<u64>(header.screenshot_offset) + header.screenshot_size) > header_size)
|
|
{
|
|
// doesn't have a screenshot
|
|
return false;
|
|
}
|
|
|
|
*width = header.screenshot_width;
|
|
*height = header.screenshot_height;
|
|
pixels->resize(header.screenshot_width * header.screenshot_height);
|
|
std::memcpy(pixels->data(), header_bits.get() + header.screenshot_offset, header.screenshot_size);
|
|
return true;
|
|
}
|
|
|
|
bool GSDumpFile::ReadFile(Error* error)
|
|
{
|
|
u32 ss;
|
|
if (Read(&m_crc, sizeof(m_crc)) != sizeof(m_crc) || Read(&ss, sizeof(ss)) != sizeof(ss))
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read header."));
|
|
return false;
|
|
}
|
|
|
|
m_state_data.resize(ss);
|
|
if (Read(m_state_data.data(), ss) != ss)
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read state data."));
|
|
return false;
|
|
}
|
|
|
|
// Pull serial out of new header, if present.
|
|
if (m_crc == 0xFFFFFFFFu)
|
|
{
|
|
GSDumpHeader header;
|
|
if (m_state_data.size() < sizeof(header))
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "GSDump header is corrupted."));
|
|
return false;
|
|
}
|
|
|
|
std::memcpy(&header, m_state_data.data(), sizeof(header));
|
|
|
|
m_crc = header.crc;
|
|
|
|
if (header.serial_size > 0)
|
|
{
|
|
if (header.serial_offset > ss || (static_cast<u64>(header.serial_offset) + header.serial_size) > ss)
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "GSDump header is corrupted."));
|
|
return false;
|
|
}
|
|
|
|
if (header.serial_size > 0)
|
|
m_serial.assign(reinterpret_cast<const char*>(m_state_data.data()) + header.serial_offset, header.serial_size);
|
|
}
|
|
|
|
// Read the real state data
|
|
m_state_data.resize(header.state_size);
|
|
if (Read(m_state_data.data(), header.state_size) != header.state_size)
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read real state data"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_regs_data.resize(8192);
|
|
if (Read(m_regs_data.data(), m_regs_data.size()) != m_regs_data.size())
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read regs data."));
|
|
return false;
|
|
}
|
|
|
|
// read all the packet data in
|
|
// TODO: make this suck less by getting the full/extracted size and preallocating
|
|
for (;;)
|
|
{
|
|
const size_t packet_data_size = m_packet_data.size();
|
|
m_packet_data.resize(std::max<size_t>(packet_data_size * 2, 8 * _1mb));
|
|
|
|
const size_t read_size = m_packet_data.size() - packet_data_size;
|
|
const size_t read = Read(m_packet_data.data() + packet_data_size, read_size);
|
|
if (read != read_size)
|
|
{
|
|
if (!IsEof())
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read packet."));
|
|
return false;
|
|
}
|
|
|
|
m_packet_data.resize(packet_data_size + read);
|
|
m_packet_data.shrink_to_fit();
|
|
break;
|
|
}
|
|
}
|
|
|
|
u8* data = m_packet_data.data();
|
|
size_t remaining = m_packet_data.size();
|
|
|
|
#define GET_BYTE(dst) \
|
|
do \
|
|
{ \
|
|
if (remaining < sizeof(u8)) \
|
|
{ \
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read byte.")); \
|
|
return false; \
|
|
} \
|
|
std::memcpy(dst, data, sizeof(u8)); \
|
|
data++; \
|
|
remaining--; \
|
|
} while (0)
|
|
#define GET_WORD(dst) \
|
|
do \
|
|
{ \
|
|
if (remaining < sizeof(u32)) \
|
|
{ \
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpFile", "Failed to read word.")); \
|
|
return false; \
|
|
} \
|
|
std::memcpy(dst, data, sizeof(u32)); \
|
|
data += sizeof(u32); \
|
|
remaining -= sizeof(u32); \
|
|
} while (0)
|
|
|
|
while (remaining > 0)
|
|
{
|
|
GSData packet = {};
|
|
packet.path = GSTransferPath::Dummy;
|
|
GET_BYTE(&packet.id);
|
|
|
|
switch (packet.id)
|
|
{
|
|
case GSType::Transfer:
|
|
GET_BYTE(&packet.path);
|
|
GET_WORD(&packet.length);
|
|
break;
|
|
case GSType::VSync:
|
|
packet.length = 1;
|
|
break;
|
|
case GSType::ReadFIFO2:
|
|
packet.length = 4;
|
|
break;
|
|
case GSType::Registers:
|
|
packet.length = 8192;
|
|
break;
|
|
default:
|
|
Error::SetStringFmt(error,
|
|
TRANSLATE_FS("GSDumpFile", "Unknown packet type {}"), static_cast<u32>(packet.id));
|
|
return false;
|
|
}
|
|
|
|
if (packet.length > 0)
|
|
{
|
|
if (remaining < packet.length)
|
|
{
|
|
// There's apparently some "bad" dumps out there that are missing bytes on the end..
|
|
// The "safest" option here is to discard the last packet, since that has less risk
|
|
// of leaving the GS in the middle of a command.
|
|
Console.Error("(GSDump) Dropping last packet of %u bytes (we only have %u bytes)",
|
|
static_cast<u32>(packet.length), static_cast<u32>(remaining));
|
|
break;
|
|
}
|
|
|
|
packet.data = data;
|
|
data += packet.length;
|
|
remaining -= packet.length;
|
|
}
|
|
|
|
m_dump_packets.push_back(std::move(packet));
|
|
}
|
|
|
|
#undef GET_WORD
|
|
#undef GET_BYTE
|
|
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************/
|
|
|
|
static std::once_flag s_lzma_crc_table_init;
|
|
|
|
void GSInit7ZCRCTables()
|
|
{
|
|
std::call_once(s_lzma_crc_table_init, []() {
|
|
CrcGenerateTable();
|
|
Crc64GenerateTable();
|
|
});
|
|
}
|
|
|
|
namespace
|
|
{
|
|
class GSDumpLzma final : public GSDumpFile
|
|
{
|
|
public:
|
|
GSDumpLzma();
|
|
~GSDumpLzma() override;
|
|
|
|
protected:
|
|
bool Open(FileSystem::ManagedCFilePtr fp, Error* error) override;
|
|
bool IsEof() override;
|
|
size_t Read(void* ptr, size_t size) override;
|
|
|
|
private:
|
|
static constexpr size_t kInputBufSize = static_cast<size_t>(1) << 18;
|
|
|
|
struct Block
|
|
{
|
|
size_t file_offset;
|
|
size_t stream_offset;
|
|
size_t compressed_size;
|
|
size_t uncompressed_size;
|
|
CXzStreamFlags stream_flags;
|
|
};
|
|
|
|
bool DecompressNextBlock();
|
|
|
|
|
|
std::vector<Block> m_blocks;
|
|
size_t m_stream_size = 0;
|
|
|
|
DynamicHeapArray<u8, 64> m_block_buffer;
|
|
size_t m_block_index = 0;
|
|
size_t m_block_size = 0;
|
|
size_t m_block_pos = 0;
|
|
|
|
DynamicHeapArray<u8, 64> m_block_read_buffer;
|
|
alignas(__cachelinesize) CXzUnpacker m_unpacker = {};
|
|
};
|
|
|
|
GSDumpLzma::GSDumpLzma() = default;
|
|
|
|
GSDumpLzma::~GSDumpLzma()
|
|
{
|
|
XzUnpacker_Free(&m_unpacker);
|
|
}
|
|
|
|
bool GSDumpLzma::Open(FileSystem::ManagedCFilePtr fp, Error* error)
|
|
{
|
|
m_fp = std::move(fp);
|
|
|
|
GSInit7ZCRCTables();
|
|
|
|
struct MyFileInStream
|
|
{
|
|
ISeekInStream vt;
|
|
std::FILE* fp;
|
|
};
|
|
|
|
MyFileInStream fis = {
|
|
{.Read = [](const ISeekInStream* p, void* buf, size_t* size) -> SRes {
|
|
MyFileInStream* fis = Z7_CONTAINER_FROM_VTBL(p, MyFileInStream, vt);
|
|
const size_t size_to_read = *size;
|
|
const auto bytes_read = std::fread(buf, 1, size_to_read, fis->fp);
|
|
*size = (bytes_read >= 0) ? bytes_read : 0;
|
|
return (bytes_read == size_to_read) ? SZ_OK : SZ_ERROR_READ;
|
|
},
|
|
.Seek = [](const ISeekInStream* p, Int64* pos, ESzSeek origin) -> SRes {
|
|
MyFileInStream* fis = Z7_CONTAINER_FROM_VTBL(p, MyFileInStream, vt);
|
|
static_assert(SZ_SEEK_CUR == SEEK_CUR && SZ_SEEK_SET == SEEK_SET && SZ_SEEK_END == SEEK_END);
|
|
if (FileSystem::FSeek64(fis->fp, *pos, static_cast<int>(origin)) != 0)
|
|
return SZ_ERROR_READ;
|
|
|
|
const s64 new_pos = FileSystem::FTell64(fis->fp);
|
|
if (new_pos < 0)
|
|
return SZ_ERROR_READ;
|
|
|
|
*pos = new_pos;
|
|
return SZ_OK;
|
|
}},
|
|
m_fp.get()};
|
|
|
|
CLookToRead2 look_stream = {};
|
|
LookToRead2_INIT(&look_stream);
|
|
LookToRead2_CreateVTable(&look_stream, False);
|
|
look_stream.realStream = &fis.vt;
|
|
look_stream.bufSize = kInputBufSize;
|
|
look_stream.buf = static_cast<Byte*>(ISzAlloc_Alloc(&g_Alloc, kInputBufSize));
|
|
if (!look_stream.buf)
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpLzma", "Failed to allocate lookahead buffer."));
|
|
return false;
|
|
}
|
|
ScopedGuard guard = [&look_stream]() {
|
|
if (look_stream.buf)
|
|
ISzAlloc_Free(&g_Alloc, look_stream.buf);
|
|
};
|
|
|
|
// Read blocks
|
|
CXzs xzs;
|
|
Xzs_Construct(&xzs);
|
|
const ScopedGuard xzs_guard([&xzs]() {
|
|
Xzs_Free(&xzs, &g_Alloc);
|
|
});
|
|
|
|
const s64 file_size = FileSystem::FSize64(m_fp.get());
|
|
Int64 start_pos = file_size;
|
|
SRes res = Xzs_ReadBackward(&xzs, &look_stream.vt, &start_pos, nullptr, &g_Alloc);
|
|
if (res != SZ_OK)
|
|
{
|
|
Error::SetStringFmt(error, TRANSLATE_FS("GSDumpLzma", "Xzs_ReadBackward() failed: {}."), res);
|
|
return false;
|
|
}
|
|
|
|
const size_t num_blocks = Xzs_GetNumBlocks(&xzs);
|
|
if (num_blocks == 0)
|
|
{
|
|
Error::SetString(error, TRANSLATE_STR("GSDumpLzma", "Stream has no blocks."));
|
|
return false;
|
|
}
|
|
|
|
m_blocks.reserve(num_blocks);
|
|
for (int sn = xzs.num - 1; sn >= 0; sn--)
|
|
{
|
|
const CXzStream& stream = xzs.streams[sn];
|
|
size_t src_offset = stream.startOffset + XZ_STREAM_HEADER_SIZE;
|
|
for (size_t bn = 0; bn < stream.numBlocks; bn++)
|
|
{
|
|
const CXzBlockSizes& block = stream.blocks[bn];
|
|
|
|
Block out_block;
|
|
out_block.file_offset = src_offset;
|
|
out_block.stream_offset = m_stream_size;
|
|
out_block.compressed_size = std::min<size_t>(Common::AlignUpPow2(block.totalSize, 4),
|
|
static_cast<size_t>(file_size - static_cast<s64>(src_offset))); // LZMA blocks are 4 byte aligned?
|
|
out_block.uncompressed_size = block.unpackSize;
|
|
out_block.stream_flags = stream.flags;
|
|
m_stream_size += out_block.uncompressed_size;
|
|
src_offset += out_block.compressed_size;
|
|
m_blocks.push_back(std::move(out_block));
|
|
}
|
|
}
|
|
|
|
DevCon.WriteLnFmt("XZ stream is {} bytes across {} blocks", m_stream_size, m_blocks.size());
|
|
XzUnpacker_Construct(&m_unpacker, &g_Alloc);
|
|
return true;
|
|
}
|
|
|
|
bool GSDumpLzma::DecompressNextBlock()
|
|
{
|
|
if (m_block_index == m_blocks.size())
|
|
return false;
|
|
|
|
const Block& block = m_blocks[m_block_index];
|
|
|
|
if (block.compressed_size > m_block_read_buffer.size())
|
|
m_block_read_buffer.resize(Common::AlignUpPow2(block.compressed_size, _128kb));
|
|
|
|
if (FileSystem::FSeek64(m_fp.get(), static_cast<s64>(block.file_offset), SEEK_SET) != 0 ||
|
|
std::fread(m_block_read_buffer.data(), block.compressed_size, 1, m_fp.get()) != 1)
|
|
{
|
|
Console.ErrorFmt("Failed to read {} bytes from offset {}", block.file_offset, block.compressed_size);
|
|
return false;
|
|
}
|
|
|
|
if (block.uncompressed_size > m_block_buffer.size())
|
|
m_block_buffer.resize(Common::AlignUpPow2(block.uncompressed_size, _128kb));
|
|
|
|
XzUnpacker_Init(&m_unpacker);
|
|
m_unpacker.streamFlags = block.stream_flags;
|
|
XzUnpacker_PrepareToRandomBlockDecoding(&m_unpacker);
|
|
XzUnpacker_SetOutBuf(&m_unpacker, m_block_buffer.data(), block.uncompressed_size);
|
|
SizeT out_uncompressed_size = block.uncompressed_size;
|
|
SizeT out_compressed_size = block.compressed_size;
|
|
|
|
ECoderStatus status;
|
|
const SRes res = XzUnpacker_Code(&m_unpacker, nullptr, &out_uncompressed_size,
|
|
m_block_read_buffer.data(), &out_compressed_size, true, CODER_FINISH_END, &status);
|
|
if (res != SZ_OK || status != CODER_STATUS_FINISHED_WITH_MARK) [[unlikely]]
|
|
{
|
|
Console.ErrorFmt("XzUnpacker_Code() failed: {} (status {})", res, static_cast<unsigned>(status));
|
|
return false;
|
|
}
|
|
|
|
if (out_compressed_size != block.compressed_size || out_uncompressed_size != block.uncompressed_size)
|
|
{
|
|
Console.WarningFmt("Decompress size mismatch: {}/{} vs {}/{}", out_compressed_size, out_uncompressed_size,
|
|
block.compressed_size, block.uncompressed_size);
|
|
}
|
|
|
|
m_block_size = out_uncompressed_size;
|
|
m_block_pos = 0;
|
|
m_block_index++;
|
|
return true;
|
|
}
|
|
|
|
bool GSDumpLzma::IsEof()
|
|
{
|
|
return (m_block_pos == m_block_size && m_block_index == m_blocks.size());
|
|
}
|
|
|
|
size_t GSDumpLzma::Read(void* ptr, size_t size)
|
|
{
|
|
u8* dst = static_cast<u8*>(ptr);
|
|
size_t remain = size;
|
|
while (remain > 0)
|
|
{
|
|
if (m_block_size == m_block_pos && !DecompressNextBlock()) [[unlikely]]
|
|
break;
|
|
|
|
const size_t avail = (m_block_size - m_block_pos);
|
|
const size_t read = std::min(avail, remain);
|
|
pxAssert(avail > 0 && read > 0);
|
|
std::memcpy(dst, &m_block_buffer[m_block_pos], read);
|
|
dst += read;
|
|
remain -= read;
|
|
m_block_pos += read;
|
|
}
|
|
|
|
return size - remain;
|
|
}
|
|
|
|
/******************************************************************/
|
|
|
|
class GSDumpDecompressZst final : public GSDumpFile
|
|
{
|
|
static constexpr u32 INPUT_BUFFER_SIZE = 512 * _1kb;
|
|
static constexpr u32 OUTPUT_BUFFER_SIZE = 2 * _1mb;
|
|
|
|
ZSTD_DStream* m_strm = nullptr;
|
|
ZSTD_inBuffer m_inbuf = {};
|
|
|
|
uint8_t* m_area = nullptr;
|
|
|
|
size_t m_avail = 0;
|
|
size_t m_start = 0;
|
|
|
|
bool Decompress();
|
|
|
|
public:
|
|
GSDumpDecompressZst();
|
|
~GSDumpDecompressZst() override;
|
|
|
|
bool Open(FileSystem::ManagedCFilePtr fp, Error* error) override;
|
|
bool IsEof() override;
|
|
size_t Read(void* ptr, size_t size) override;
|
|
};
|
|
|
|
GSDumpDecompressZst::GSDumpDecompressZst() = default;
|
|
|
|
GSDumpDecompressZst::~GSDumpDecompressZst()
|
|
{
|
|
if (m_strm)
|
|
ZSTD_freeDStream(m_strm);
|
|
|
|
if (m_inbuf.src)
|
|
_aligned_free(const_cast<void*>(m_inbuf.src));
|
|
if (m_area)
|
|
_aligned_free(m_area);
|
|
}
|
|
|
|
bool GSDumpDecompressZst::Open(FileSystem::ManagedCFilePtr fp, Error* error)
|
|
{
|
|
m_fp = std::move(fp);
|
|
m_strm = ZSTD_createDStream();
|
|
|
|
m_area = static_cast<uint8_t*>(_aligned_malloc(OUTPUT_BUFFER_SIZE, 32));
|
|
m_inbuf.src = static_cast<uint8_t*>(_aligned_malloc(INPUT_BUFFER_SIZE, 32));
|
|
m_inbuf.pos = 0;
|
|
m_inbuf.size = 0;
|
|
m_avail = 0;
|
|
m_start = 0;
|
|
return true;
|
|
}
|
|
|
|
bool 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 && !std::feof(m_fp.get()))
|
|
{
|
|
m_inbuf.size = fread(const_cast<void*>(m_inbuf.src), 1, INPUT_BUFFER_SIZE, m_fp.get());
|
|
m_inbuf.pos = 0;
|
|
|
|
if (ferror(m_fp.get()))
|
|
{
|
|
Console.Error("Zst read error: %s", strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const size_t ret = ZSTD_decompressStream(m_strm, &outbuf, &m_inbuf);
|
|
if (ZSTD_isError(ret))
|
|
{
|
|
Console.Error("Decoder error: (error code %s)", ZSTD_getErrorName(ret));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_start = 0;
|
|
m_avail = outbuf.pos;
|
|
return true;
|
|
}
|
|
|
|
bool GSDumpDecompressZst::IsEof()
|
|
{
|
|
return feof(m_fp.get()) && 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 = static_cast<uint8_t*>(ptr);
|
|
while (size && !IsEof())
|
|
{
|
|
if (m_avail == 0)
|
|
{
|
|
if (!Decompress()) [[unlikely]]
|
|
break;
|
|
}
|
|
|
|
const size_t l = std::min(size, m_avail);
|
|
std::memcpy(dst + off, m_area + m_start, l);
|
|
m_avail -= l;
|
|
size -= l;
|
|
m_start += l;
|
|
off += l;
|
|
}
|
|
|
|
return off;
|
|
}
|
|
|
|
/******************************************************************/
|
|
|
|
class GSDumpRaw final : public GSDumpFile
|
|
{
|
|
public:
|
|
GSDumpRaw();
|
|
~GSDumpRaw() override;
|
|
|
|
bool Open(FileSystem::ManagedCFilePtr fp, Error* error) override;
|
|
bool IsEof() override;
|
|
size_t Read(void* ptr, size_t size) override;
|
|
};
|
|
|
|
GSDumpRaw::GSDumpRaw() = default;
|
|
|
|
GSDumpRaw::~GSDumpRaw() = default;
|
|
|
|
bool GSDumpRaw::Open(FileSystem::ManagedCFilePtr fp, Error* error)
|
|
{
|
|
m_fp = std::move(fp);
|
|
return true;
|
|
}
|
|
|
|
bool GSDumpRaw::IsEof()
|
|
{
|
|
return !!feof(m_fp.get());
|
|
}
|
|
|
|
size_t GSDumpRaw::Read(void* ptr, size_t size)
|
|
{
|
|
size_t ret = fread(ptr, 1, size, m_fp.get());
|
|
if (ret != size && ferror(m_fp.get()))
|
|
{
|
|
fprintf(stderr, "GSDumpRaw:: Read error (%zu/%zu)\n", ret, size);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
} // namespace
|
|
|
|
/******************************************************************/
|
|
|
|
std::unique_ptr<GSDumpFile> GSDumpFile::OpenGSDump(const char* filename, Error* error)
|
|
{
|
|
FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(filename, "rb", error);
|
|
if (!fp)
|
|
return nullptr;
|
|
|
|
std::unique_ptr<GSDumpFile> file;
|
|
if (StringUtil::EndsWithNoCase(filename, ".xz"))
|
|
file = std::make_unique<GSDumpLzma>();
|
|
else if (StringUtil::EndsWithNoCase(filename, ".zst"))
|
|
file = std::make_unique<GSDumpDecompressZst>();
|
|
else
|
|
file = std::make_unique<GSDumpRaw>();
|
|
|
|
if (!file->Open(std::move(fp), error))
|
|
file = {};
|
|
|
|
return file;
|
|
}
|