diff --git a/.gitmodules b/.gitmodules
index bac18a1143..8e701abf03 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -63,3 +63,6 @@
[submodule "Externals/fmt/fmt"]
path = Externals/fmt/fmt
url = https://github.com/fmtlib/fmt.git
+[submodule "Externals/lz4/lz4"]
+ path = Externals/lz4/lz4
+ url = https://github.com/lz4/lz4
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b6e3601ce2..b1eede22aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -682,6 +682,8 @@ dolphin_find_optional_system_library_pkgconfig(MINIZIP minizip>=3.0.0 minizip::m
dolphin_find_optional_system_library(LZO Externals/LZO)
+dolphin_find_optional_system_library_pkgconfig(lz4 liblz4>=1.8 LZ4::LZ4 Externals/lz4)
+
dolphin_find_optional_system_library_pkgconfig(SPNG spng spng::spng Externals/libspng)
# Using static FreeSurround from Externals
diff --git a/Externals/lz4/CMakeLists.txt b/Externals/lz4/CMakeLists.txt
new file mode 100644
index 0000000000..065e52593a
--- /dev/null
+++ b/Externals/lz4/CMakeLists.txt
@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 3.13)
+
+# We only want the static library, nothing else.
+set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
+set(LZ4_BUILD_CLI OFF)
+set(LZ4_BUILD_LEGACY_LZ4C OFF)
+set(LZ4_BUNDLED_MODE ON)
+
+add_subdirectory(lz4/build/cmake)
+
+dolphin_disable_warnings_msvc(lz4_static)
+add_library(LZ4::LZ4 ALIAS lz4_static)
+
diff --git a/Externals/lz4/LZ4.vcxproj b/Externals/lz4/LZ4.vcxproj
new file mode 100644
index 0000000000..4bb1b83268
--- /dev/null
+++ b/Externals/lz4/LZ4.vcxproj
@@ -0,0 +1,34 @@
+
+
+
+
+
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Externals/lz4/exports.props b/Externals/lz4/exports.props
new file mode 100644
index 0000000000..58d3c4d3ec
--- /dev/null
+++ b/Externals/lz4/exports.props
@@ -0,0 +1,13 @@
+
+
+
+
+ $(ExternalsDir)lz4\lz4\lib;%(AdditionalIncludeDirectories)
+
+
+
+
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}
+
+
+
diff --git a/Externals/lz4/lz4 b/Externals/lz4/lz4
new file mode 160000
index 0000000000..5fc0630a0e
--- /dev/null
+++ b/Externals/lz4/lz4
@@ -0,0 +1 @@
+Subproject commit 5fc0630a0ed55e9755b0fe3990cd021ae1c3edc1
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 60f3c54a84..4a40eb7ec8 100644
--- a/Source/Core/Core/CMakeLists.txt
+++ b/Source/Core/Core/CMakeLists.txt
@@ -627,6 +627,7 @@ PRIVATE
FatFs
fmt::fmt
LZO::LZO
+ LZ4::LZ4
ZLIB::ZLIB
)
diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp
index c298dd121e..ce0c636f66 100644
--- a/Source/Core/Core/State.cpp
+++ b/Source/Core/Core/State.cpp
@@ -16,6 +16,7 @@
#include
+#include
#include
#include "Common/ChunkFile.h"
@@ -60,11 +61,6 @@ static const u32 OUT_LEN = IN_LEN + (IN_LEN / 16) + 64 + 3;
static unsigned char __LZO_MMODEL out[OUT_LEN];
-#define HEAP_ALLOC(var, size) \
- lzo_align_t __LZO_MMODEL var[((size) + (sizeof(lzo_align_t) - 1)) / sizeof(lzo_align_t)]
-
-static HEAP_ALLOC(wrkmem, LZO1X_1_MEM_COMPRESS);
-
static AfterLoadCallbackFunc s_on_after_load_callback;
// Temporary undo state buffer
@@ -96,7 +92,15 @@ static size_t s_state_writes_in_queue;
static std::condition_variable s_state_write_queue_is_empty;
// Don't forget to increase this after doing changes on the savestate system
-constexpr u32 STATE_VERSION = 162; // Last changed in PR 11767
+constexpr u32 STATE_VERSION = 163; // Last changed in PR 12217
+
+// Increase this if the StateExtendedHeader definition changes
+constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
+
+// Change this if we ever need to store more data in the extended header
+constexpr u32 COMPRESSED_DATA_OFFSET = 0;
+
+constexpr u32 COOKIE_BASE = 0xBAADBABE;
// Maps savestate versions to Dolphin versions.
// Versions after 42 don't need to be added to this list,
@@ -128,60 +132,8 @@ void EnableCompression(bool compression)
s_use_compression = compression;
}
-// Returns true if state version matches current Dolphin state version, false otherwise.
-static bool DoStateVersion(PointerWrap& p, std::string* version_created_by)
-{
- u32 version = STATE_VERSION;
- {
- static const u32 COOKIE_BASE = 0xBAADBABE;
- u32 cookie = version + COOKIE_BASE;
- p.Do(cookie);
- version = cookie - COOKIE_BASE;
- }
-
- *version_created_by = Common::GetScmRevStr();
- if (version > 42)
- p.Do(*version_created_by);
- else
- version_created_by->clear();
-
- if (version != STATE_VERSION)
- {
- if (version_created_by->empty() && s_old_versions.count(version))
- {
- // The savestate is from an old version that doesn't
- // save the Dolphin version number to savestates, but
- // by looking up the savestate version number, it is possible
- // to know approximately which Dolphin version was used.
-
- std::pair version_range = s_old_versions.find(version)->second;
- std::string oldest_version = version_range.first;
- std::string newest_version = version_range.second;
-
- *version_created_by = "Dolphin " + oldest_version + " - " + newest_version;
- }
-
- return false;
- }
-
- p.DoMarker("Version");
- return true;
-}
-
static void DoState(PointerWrap& p)
{
- std::string version_created_by;
- if (!DoStateVersion(p, &version_created_by))
- {
- const std::string message =
- version_created_by.empty() ?
- "This savestate was created using an incompatible version of Dolphin" :
- "This savestate was created using the incompatible version " + version_created_by;
- Core::DisplayMessage(message, OSD::Duration::NORMAL);
- p.SetMeasureMode();
- return;
- }
-
bool is_wii = SConfig::GetInstance().bWii || SConfig::GetInstance().m_is_mios;
const bool is_wii_currently = is_wii;
p.Do(is_wii);
@@ -341,7 +293,7 @@ static std::map GetSavedStates()
{
if (ReadHeader(filename, header))
{
- double d = GetSystemTimeAsDouble() - header.time;
+ double d = GetSystemTimeAsDouble() - header.legacy_header.time;
// increase time until unique value is obtained
while (m.find(d) != m.end())
@@ -354,6 +306,72 @@ static std::map GetSavedStates()
return m;
}
+static void CompressBufferToFile(const u8* raw_buffer, u64 size, File::IOFile& f)
+{
+ u64 total_bytes_compressed = 0;
+
+ while (true)
+ {
+ u64 bytes_left_to_compress = size - total_bytes_compressed;
+
+ int bytes_to_compress =
+ static_cast(std::min(static_cast(LZ4_MAX_INPUT_SIZE), bytes_left_to_compress));
+ int compressed_buffer_size = LZ4_compressBound(bytes_to_compress);
+ auto compressed_buffer = std::make_unique(compressed_buffer_size);
+ s32 compressed_len =
+ LZ4_compress_default(reinterpret_cast(raw_buffer) + total_bytes_compressed,
+ compressed_buffer.get(), bytes_to_compress, compressed_buffer_size);
+
+ if (compressed_len == 0)
+ {
+ PanicAlertFmtT("Internal LZ4 Error - compression failed");
+ break;
+ }
+
+ // The size of the data to write is 'compressed_len'
+ f.WriteArray(&compressed_len, 1);
+ f.WriteBytes(compressed_buffer.get(), compressed_len);
+
+ total_bytes_compressed += bytes_to_compress;
+ if (total_bytes_compressed == size)
+ break;
+ }
+}
+
+static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size)
+{
+ StateExtendedBaseHeader& base_header = extended_header.base_header;
+ base_header.header_version = EXTENDED_HEADER_VERSION;
+ base_header.compression_type =
+ s_use_compression ? CompressionType::LZ4 : CompressionType::Uncompressed;
+ base_header.payload_offset = COMPRESSED_DATA_OFFSET;
+ base_header.uncompressed_size = uncompressed_size;
+
+ // If more fields are added to StateExtendedHeader, set them here.
+}
+
+static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
+{
+ StateHeader header{};
+ SConfig::GetInstance().GetGameID().copy(header.legacy_header.game_id,
+ std::size(header.legacy_header.game_id));
+ header.legacy_header.time = GetSystemTimeAsDouble();
+
+ header.version_header.version_cookie = COOKIE_BASE + STATE_VERSION;
+ header.version_string = Common::GetScmRevStr();
+ header.version_header.version_string_length = static_cast(header.version_string.length());
+
+ StateExtendedHeader extended_header{};
+ CreateExtendedHeader(extended_header, uncompressed_size);
+
+ f.WriteArray(&header.legacy_header, 1);
+ f.WriteArray(&header.version_header, 1);
+ f.WriteString(header.version_string);
+
+ f.WriteArray(&extended_header.base_header, 1);
+ // If StateExtendedHeader is amended to include more than the base, add WriteBytes() calls here.
+}
+
static void CompressAndDumpState(CompressAndDumpState_args& save_args)
{
const u8* const buffer_data = save_args.buffer_vector.data();
@@ -378,48 +396,12 @@ static void CompressAndDumpState(CompressAndDumpState_args& save_args)
return;
}
- // Setting up the header
- StateHeader header{};
- SConfig::GetInstance().GetGameID().copy(header.gameID, std::size(header.gameID));
- header.size = s_use_compression ? (u32)buffer_size : 0;
- header.time = GetSystemTimeAsDouble();
+ WriteHeadersToFile(buffer_size, f);
- f.WriteArray(&header, 1);
-
- if (header.size != 0) // non-zero header size means the state is compressed
- {
- lzo_uint i = 0;
- while (true)
- {
- lzo_uint32 cur_len = 0;
- lzo_uint out_len = 0;
-
- if ((i + IN_LEN) >= buffer_size)
- {
- cur_len = (lzo_uint32)(buffer_size - i);
- }
- else
- {
- cur_len = IN_LEN;
- }
-
- if (lzo1x_1_compress(buffer_data + i, cur_len, out, &out_len, wrkmem) != LZO_E_OK)
- PanicAlertFmtT("Internal LZO Error - compression failed");
-
- // The size of the data to write is 'out_len'
- f.WriteArray((lzo_uint32*)&out_len, 1);
- f.WriteBytes(out, out_len);
-
- if (cur_len != IN_LEN)
- break;
-
- i += cur_len;
- }
- }
- else // uncompressed
- {
+ if (s_use_compression)
+ CompressBufferToFile(buffer_data, buffer_size, f);
+ else
f.WriteBytes(buffer_data, buffer_size);
- }
const std::string last_state_filename = File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav";
const std::string last_state_dtmname = last_state_filename + ".dtm";
@@ -525,13 +507,108 @@ void SaveAs(const std::string& filename, bool wait)
true);
}
+static bool GetVersionFromLZO(StateHeader& header, File::IOFile& f)
+{
+ // Just read the first block, since it will contain the full revision string
+ lzo_uint32 cur_len = 0; // size of compressed bytes
+ lzo_uint new_len = 0; // size of uncompressed bytes
+ std::vector buffer;
+ buffer.resize(header.legacy_header.lzo_size);
+
+ if (!f.ReadArray(&cur_len, 1) || !f.ReadBytes(out, cur_len))
+ return false;
+
+ const int res = lzo1x_decompress(out, cur_len, buffer.data(), &new_len, nullptr);
+ if (res != LZO_E_OK)
+ {
+ // This doesn't seem to happen anymore.
+ PanicAlertFmtT("Internal LZO Error - decompression failed ({0}) ({1}) \n"
+ "Unable to retrieve outdated savestate version info.",
+ res, new_len);
+ return false;
+ }
+
+ // Read in cookie and string length
+ if (buffer.size() >= sizeof(StateHeaderVersion))
+ {
+ memcpy(&header.version_header, buffer.data(), sizeof(StateHeaderVersion));
+ }
+ else
+ {
+ PanicAlertFmtT("Internal LZO Error - failed to parse decompressed version cookie and version "
+ "string length ({0})",
+ buffer.size());
+ return false;
+ }
+
+ // Read in the string
+ if (buffer.size() >= sizeof(StateHeaderVersion) + header.version_header.version_string_length)
+ {
+ auto version_buffer = std::make_unique(header.version_header.version_string_length);
+ memcpy(version_buffer.get(), buffer.data() + sizeof(StateHeaderVersion),
+ header.version_header.version_string_length);
+ header.version_string =
+ std::string(version_buffer.get(), header.version_header.version_string_length);
+ }
+ else
+ {
+ PanicAlertFmtT("Internal LZO Error - failed to parse decompressed version string ({0} / {1})",
+ header.version_header.version_string_length, buffer.size());
+ return false;
+ }
+
+ return true;
+}
+
+static bool ReadStateHeaderFromFile(StateHeader& header, File::IOFile& f)
+{
+ if (!f.IsOpen())
+ {
+ Core::DisplayMessage("State not found", 2000);
+ return false;
+ }
+
+ if (!f.ReadArray(&header.legacy_header, 1))
+ {
+ Core::DisplayMessage("Failed to read state legacy header", 2000);
+ return false;
+ }
+
+ if (header.legacy_header.lzo_size != 0)
+ {
+ // Parse out version from legacy LZO compressed states
+ if (!GetVersionFromLZO(header, f))
+ return false;
+ }
+ else
+ {
+ if (!f.ReadArray(&header.version_header, 1))
+ {
+ Core::DisplayMessage("Failed to read state version header", 2000);
+ return false;
+ }
+
+ auto version_buffer = std::make_unique(header.version_header.version_string_length);
+ if (!f.ReadBytes(version_buffer.get(), header.version_header.version_string_length))
+ {
+ Core::DisplayMessage("Failed to read state version string", 2000);
+ return false;
+ }
+
+ header.version_string =
+ std::string(version_buffer.get(), header.version_header.version_string_length);
+ }
+
+ return true;
+}
+
bool ReadHeader(const std::string& filename, StateHeader& header)
{
// ensure that the savestate write thread isn't moving around states while we do this
std::lock_guard lk(s_save_thread_mutex);
File::IOFile f(filename, "rb");
- return f.ReadArray(&header, 1);
+ return ReadStateHeaderFromFile(header, f);
}
std::string GetInfoStringOfSlot(int slot, bool translate)
@@ -544,7 +621,7 @@ std::string GetInfoStringOfSlot(int slot, bool translate)
if (!ReadHeader(filename, header))
return translate ? Common::GetStringT("Unknown") : "Unknown";
- return SystemTimeAsDoubleToString(header.time);
+ return SystemTimeAsDoubleToString(header.legacy_header.time);
}
u64 GetUnixTimeOfSlot(int slot)
@@ -554,7 +631,111 @@ u64 GetUnixTimeOfSlot(int slot)
return 0;
constexpr u64 MS_PER_SEC = 1000;
- return static_cast(header.time * MS_PER_SEC) + (DOUBLE_TIME_OFFSET * MS_PER_SEC);
+ return static_cast(header.legacy_header.time * MS_PER_SEC) +
+ (DOUBLE_TIME_OFFSET * MS_PER_SEC);
+}
+
+static bool DecompressLZ4(std::vector& raw_buffer, u64 size, File::IOFile& f)
+{
+ raw_buffer.resize(size);
+
+ u64 total_bytes_read = 0;
+ while (true)
+ {
+ s32 compressed_data_len;
+ if (!f.ReadArray(&compressed_data_len, 1))
+ {
+ PanicAlertFmt("Could not read state data length");
+ return false;
+ }
+
+ if (compressed_data_len <= 0)
+ {
+ PanicAlertFmtT("Internal LZ4 Error - Tried decompressing {0} bytes", compressed_data_len);
+ return false;
+ }
+
+ auto compressed_data = std::make_unique(compressed_data_len);
+ if (!f.ReadBytes(compressed_data.get(), compressed_data_len))
+ {
+ PanicAlertFmt("Could not read state data");
+ return false;
+ }
+
+ u32 max_decompress_size =
+ static_cast(std::min((u64)LZ4_MAX_INPUT_SIZE, size - total_bytes_read));
+
+ int bytes_read = LZ4_decompress_safe(
+ compressed_data.get(), reinterpret_cast(raw_buffer.data()) + total_bytes_read,
+ compressed_data_len, max_decompress_size);
+
+ if (bytes_read < 0)
+ {
+ PanicAlertFmtT("Internal LZ4 Error - decompression failed ({0}, {1}, {2})", bytes_read,
+ compressed_data_len, max_decompress_size);
+ return false;
+ }
+
+ total_bytes_read += static_cast(bytes_read);
+
+ if (total_bytes_read == size)
+ {
+ return true;
+ }
+ else if (total_bytes_read > size)
+ {
+ PanicAlertFmtT("Internal LZ4 Error - payload size mismatch ({0} / {1}))", total_bytes_read,
+ size);
+ return false;
+ }
+ }
+}
+
+static bool ValidateHeaders(const StateHeader& header)
+{
+ bool success = true;
+
+ // Game ID
+ if (strncmp(SConfig::GetInstance().GetGameID().c_str(), header.legacy_header.game_id, 6))
+ {
+ Core::DisplayMessage(fmt::format("State belongs to a different game (ID {})",
+ std::string_view{header.legacy_header.game_id,
+ std::size(header.legacy_header.game_id)}),
+ 2000);
+ return false;
+ }
+
+ // Check both the state version and the revision string
+ std::string current_str = Common::GetScmRevStr();
+ std::string loaded_str = header.version_string;
+ const u32 loaded_version = header.version_header.version_cookie - COOKIE_BASE;
+
+ if (s_old_versions.count(loaded_version))
+ {
+ // This is a REALLY old version, before we started writing the version string to file
+ success = false;
+
+ std::pair version_range = s_old_versions.find(loaded_version)->second;
+ std::string oldest_version = version_range.first;
+ std::string newest_version = version_range.second;
+
+ loaded_str = "Dolphin " + oldest_version + " - " + newest_version;
+ }
+ else if (loaded_version != STATE_VERSION)
+ {
+ success = false;
+ }
+
+ if (!success)
+ {
+ const std::string message =
+ loaded_str.empty() ?
+ "This savestate was created using an incompatible version of Dolphin" :
+ "This savestate was created using the incompatible version " + loaded_str;
+ Core::DisplayMessage(message, OSD::Duration::NORMAL);
+ }
+
+ return success;
}
static void LoadFileStateData(const std::string& filename, std::vector& ret_data)
@@ -578,61 +759,61 @@ static void LoadFileStateData(const std::string& filename, std::vector& ret_
}
StateHeader header;
- if (!f.ReadArray(&header, 1))
+ if (!ReadStateHeaderFromFile(header, f) || !ValidateHeaders(header))
+ return;
+
+ StateExtendedHeader extended_header;
+ if (!f.ReadArray(&extended_header.base_header, 1))
{
- Core::DisplayMessage("State not found", 2000);
+ PanicAlertFmt("Unable to read state header");
return;
}
+ // If StateExtendedHeader is amended to include more than the base, add ReadBytes() calls here.
- if (strncmp(SConfig::GetInstance().GetGameID().c_str(), header.gameID, 6))
+ if (extended_header.base_header.header_version != EXTENDED_HEADER_VERSION)
{
- Core::DisplayMessage(fmt::format("State belongs to a different game (ID {})",
- std::string_view{header.gameID, std::size(header.gameID)}),
- 2000);
+ PanicAlertFmt("State header corrupted");
return;
}
std::vector buffer;
- if (header.size != 0) // non-zero size means the state is compressed
+ switch (extended_header.base_header.compression_type)
+ {
+ case CompressionType::LZ4:
{
Core::DisplayMessage("Decompressing State...", 500);
+ if (!DecompressLZ4(buffer, extended_header.base_header.uncompressed_size, f))
+ return;
- buffer.resize(header.size);
-
- lzo_uint i = 0;
- while (true)
- {
- lzo_uint32 cur_len = 0; // number of bytes to read
- lzo_uint new_len = 0; // number of bytes to write
-
- if (!f.ReadArray(&cur_len, 1))
- break;
-
- f.ReadBytes(out, cur_len);
- const int res = lzo1x_decompress(out, cur_len, &buffer[i], &new_len, nullptr);
- if (res != LZO_E_OK)
- {
- // This doesn't seem to happen anymore.
- PanicAlertFmtT("Internal LZO Error - decompression failed ({0}) ({1}, {2}) \n"
- "Try loading the state again",
- res, i, new_len);
- return;
- }
-
- i += new_len;
- }
+ break;
}
- else // uncompressed
+ case CompressionType::Uncompressed:
{
- const auto size = static_cast(f.GetSize() - sizeof(StateHeader));
+ u64 header_len = sizeof(StateHeaderLegacy) + sizeof(StateHeaderVersion) +
+ header.version_header.version_string_length + sizeof(StateExtendedBaseHeader) +
+ extended_header.base_header.payload_offset;
+
+ u64 file_size = f.GetSize();
+ if (file_size < header_len)
+ {
+ PanicAlertFmt("State header length corrupted");
+ return;
+ }
+
+ const auto size = static_cast(file_size - header_len);
buffer.resize(size);
- if (!f.ReadBytes(&buffer[0], size))
+ if (!f.ReadBytes(buffer.data(), size))
{
PanicAlertFmt("Error reading bytes: {0}", size);
return;
}
+ break;
+ }
+ default:
+ PanicAlertFmt("Unknown compression type {0}", extended_header.base_header.compression_type);
+ return;
}
// all good
@@ -721,9 +902,6 @@ void SetOnAfterLoadCallback(AfterLoadCallbackFunc callback)
void Init()
{
- if (lzo_init() != LZO_E_OK)
- PanicAlertFmtT("Internal LZO Error - lzo_init() failed");
-
s_save_thread.Reset("Savestate Worker", [](CompressAndDumpState_args args) {
CompressAndDumpState(args);
diff --git a/Source/Core/Core/State.h b/Source/Core/Core/State.h
index 7aa75b2a75..7d592b2d88 100644
--- a/Source/Core/Core/State.h
+++ b/Source/Core/Core/State.h
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include "Common/CommonTypes.h"
@@ -17,18 +18,62 @@ namespace State
// number of states
static const u32 NUM_STATES = 10;
-struct StateHeader
+struct StateHeaderLegacy
{
- char gameID[6];
- u16 reserved1;
- u32 size;
- u32 reserved2;
+ char game_id[6];
+ char reserved1[2];
+ u32 lzo_size = 0; // Must be zero for new states. Used to support legacy decompression algorithm.
+ char reserved2[4];
double time;
};
-constexpr size_t STATE_HEADER_SIZE = sizeof(StateHeader);
+constexpr size_t STATE_HEADER_SIZE = sizeof(StateHeaderLegacy);
static_assert(STATE_HEADER_SIZE == 24);
-static_assert(offsetof(StateHeader, size) == 8);
-static_assert(offsetof(StateHeader, time) == 16);
+static_assert(offsetof(StateHeaderLegacy, lzo_size) == 8);
+static_assert(offsetof(StateHeaderLegacy, time) == 16);
+static_assert(std::is_trivially_copyable_v);
+
+struct StateHeaderVersion
+{
+ u32 version_cookie;
+ u32 version_string_length;
+};
+static_assert(std::is_trivially_copyable_v);
+
+struct StateHeader
+{
+ StateHeaderLegacy legacy_header;
+ StateHeaderVersion version_header;
+ std::string version_string;
+};
+
+enum CompressionType : u16
+{
+ Uncompressed = 0,
+ LZ4 = 1,
+ // Add new compression types after this, as the compression type
+ // is numerically stored in the state file.
+};
+
+struct StateExtendedBaseHeader
+{
+ u16 header_version;
+ u16 compression_type;
+ u32 payload_offset;
+ u64 uncompressed_size;
+};
+constexpr size_t EXTENDED_BASE_HEADER_SIZE = sizeof(StateExtendedBaseHeader);
+static_assert(EXTENDED_BASE_HEADER_SIZE == 16);
+static_assert(offsetof(StateExtendedBaseHeader, payload_offset) == 4);
+static_assert(offsetof(StateExtendedBaseHeader, uncompressed_size) == 8);
+static_assert(std::is_trivially_copyable_v);
+
+struct StateExtendedHeader
+{
+ StateExtendedBaseHeader base_header;
+ // Feel free to add new fields here, adjusting COMPRESSED_DATA_OFFSET accordingly, as well as
+ // CreateExtendedHeader(). Add the appropriate IOFile read/write calls within LoadFileStateData()
+ // and WriteHeadersToFile()
+};
void Init();
diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj
index 827652cae7..d2071d6618 100644
--- a/Source/Core/DolphinLib.vcxproj
+++ b/Source/Core/DolphinLib.vcxproj
@@ -46,6 +46,7 @@
+
diff --git a/Source/dolphin-emu.sln b/Source/dolphin-emu.sln
index 916bc2ec73..ebb269de71 100644
--- a/Source/dolphin-emu.sln
+++ b/Source/dolphin-emu.sln
@@ -25,6 +25,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Languages", "..\Languages\L
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LZO", "..\Externals\LZO\LZO.vcxproj", "{AB993F38-C31D-4897-B139-A620C42BC565}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lz4", "..\Externals\LZ4\LZ4.vcxproj", "{9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}"
+EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "miniupnpc", "..\Externals\miniupnpc\miniupnpc.vcxproj", "{31643FDB-1BB8-4965-9DE7-000FC88D35AE}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "xxhash", "..\Externals\xxhash\xxhash.vcxproj", "{677EA016-1182-440C-9345-DC88D1E98C0C}"
@@ -173,6 +175,14 @@ Global
{AB993F38-C31D-4897-B139-A620C42BC565}.Release|ARM64.Build.0 = Release|ARM64
{AB993F38-C31D-4897-B139-A620C42BC565}.Release|x64.ActiveCfg = Release|x64
{AB993F38-C31D-4897-B139-A620C42BC565}.Release|x64.Build.0 = Release|x64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Debug|ARM64.ActiveCfg = Debug|x64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Debug|ARM64.Build.0 = Debug|x64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Debug|x64.ActiveCfg = Debug|x64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Debug|x64.Build.0 = Debug|x64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Release|ARM64.ActiveCfg = Release|ARM64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Release|ARM64.Build.0 = Release|ARM64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Release|x64.ActiveCfg = Release|x64
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476}.Release|x64.Build.0 = Release|x64
{31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|ARM64.ActiveCfg = Debug|ARM64
{31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|ARM64.Build.0 = Debug|ARM64
{31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|x64.ActiveCfg = Debug|x64
@@ -436,6 +446,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B} = {87ADDFF9-5768-4DA2-A33B-2477593D6677}
{AB993F38-C31D-4897-B139-A620C42BC565} = {87ADDFF9-5768-4DA2-A33B-2477593D6677}
+ {9092C5CC-3E71-41B3-BF68-4A7BDD8A5476} = {87ADDFF9-5768-4DA2-A33B-2477593D6677}
{31643FDB-1BB8-4965-9DE7-000FC88D35AE} = {87ADDFF9-5768-4DA2-A33B-2477593D6677}
{677EA016-1182-440C-9345-DC88D1E98C0C} = {87ADDFF9-5768-4DA2-A33B-2477593D6677}
{F6EA7144-8D64-4EBB-A13E-76DFBD911EAE} = {87ADDFF9-5768-4DA2-A33B-2477593D6677}