RVZ: Store pseudorandom junk data efficiently

This commit is contained in:
JosJuice 2020-05-10 19:21:05 +02:00
parent 1e92b54bf5
commit 4b74993374
8 changed files with 689 additions and 75 deletions

View File

@ -21,6 +21,8 @@ add_library(discio
FileSystemGCWii.h FileSystemGCWii.h
Filesystem.cpp Filesystem.cpp
Filesystem.h Filesystem.h
LaggedFibonacciGenerator.cpp
LaggedFibonacciGenerator.h
MultithreadedCompressor.h MultithreadedCompressor.h
NANDImporter.cpp NANDImporter.cpp
NANDImporter.h NANDImporter.h

View File

@ -55,6 +55,7 @@
<ClCompile Include="FileBlob.cpp" /> <ClCompile Include="FileBlob.cpp" />
<ClCompile Include="Filesystem.cpp" /> <ClCompile Include="Filesystem.cpp" />
<ClCompile Include="FileSystemGCWii.cpp" /> <ClCompile Include="FileSystemGCWii.cpp" />
<ClCompile Include="LaggedFibonacciGenerator.cpp" />
<ClCompile Include="NANDImporter.cpp" /> <ClCompile Include="NANDImporter.cpp" />
<ClCompile Include="ScrubbedBlob.cpp" /> <ClCompile Include="ScrubbedBlob.cpp" />
<ClCompile Include="TGCBlob.cpp" /> <ClCompile Include="TGCBlob.cpp" />
@ -81,6 +82,7 @@
<ClInclude Include="FileBlob.h" /> <ClInclude Include="FileBlob.h" />
<ClInclude Include="Filesystem.h" /> <ClInclude Include="Filesystem.h" />
<ClInclude Include="FileSystemGCWii.h" /> <ClInclude Include="FileSystemGCWii.h" />
<ClInclude Include="LaggedFibonacciGenerator.h" />
<ClInclude Include="MultithreadedCompressor.h" /> <ClInclude Include="MultithreadedCompressor.h" />
<ClInclude Include="NANDImporter.h" /> <ClInclude Include="NANDImporter.h" />
<ClInclude Include="ScrubbedBlob.h" /> <ClInclude Include="ScrubbedBlob.h" />

View File

@ -93,6 +93,9 @@
<ClCompile Include="WIABlob.cpp"> <ClCompile Include="WIABlob.cpp">
<Filter>Volume\Blob</Filter> <Filter>Volume\Blob</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="LaggedFibonacciGenerator.cpp">
<Filter>Volume\Blob</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="DiscScrubber.h"> <ClInclude Include="DiscScrubber.h">
@ -170,6 +173,9 @@
<ClInclude Include="WIABlob.h"> <ClInclude Include="WIABlob.h">
<Filter>Volume\Blob</Filter> <Filter>Volume\Blob</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="LaggedFibonacciGenerator.h">
<Filter>Volume\Blob</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Text Include="CMakeLists.txt" /> <Text Include="CMakeLists.txt" />

View File

@ -0,0 +1,212 @@
// This file is under the public domain.
#include "DiscIO/LaggedFibonacciGenerator.h"
#include <algorithm>
#include <cstddef>
#include <cstring>
#include "Common/Align.h"
#include "Common/Assert.h"
#include "Common/CommonTypes.h"
#include "Common/Swap.h"
namespace DiscIO
{
void LaggedFibonacciGenerator::SetSeed(const u32 seed[SEED_SIZE])
{
SetSeed(reinterpret_cast<const u8*>(seed));
}
void LaggedFibonacciGenerator::SetSeed(const u8 seed[SEED_SIZE * sizeof(u32)])
{
m_position_bytes = 0;
for (size_t i = 0; i < SEED_SIZE; ++i)
m_buffer[i] = Common::swap32(seed + i * sizeof(u32));
Initialize(false);
}
size_t LaggedFibonacciGenerator::GetSeed(const u8* data, size_t size, size_t data_offset,
u32 seed_out[SEED_SIZE])
{
if ((reinterpret_cast<uintptr_t>(data) - data_offset) % alignof(u32) != 0)
{
ASSERT(false);
return 0;
}
// For code simplicity, only include whole u32 words when regenerating the seed. It would be
// possible to get rid of this restriction and use a few additional bytes, but it's probably more
// effort than it's worth considering that junk data often starts or ends on 4-byte offsets.
const size_t bytes_to_skip = Common::AlignUp(data_offset, sizeof(u32)) - data_offset;
const u32* u32_data = reinterpret_cast<const u32*>(data + bytes_to_skip);
const size_t u32_size = (size - bytes_to_skip) / sizeof(u32);
const size_t u32_data_offset = (data_offset + bytes_to_skip) / sizeof(u32);
LaggedFibonacciGenerator lfg;
if (!GetSeed(u32_data, u32_size, u32_data_offset, &lfg, seed_out))
return false;
lfg.m_position_bytes = data_offset % (LFG_K * sizeof(u32));
const u8* end = data + size;
size_t reconstructed_bytes = 0;
while (data < end && lfg.GetByte() == *data)
{
++reconstructed_bytes;
++data;
}
return reconstructed_bytes;
}
bool LaggedFibonacciGenerator::GetSeed(const u32* data, size_t size, size_t data_offset,
LaggedFibonacciGenerator* lfg, u32 seed_out[SEED_SIZE])
{
if (size < LFG_K)
return false;
// If the data doesn't look like something we can regenerate, return early to save time
if (!std::all_of(data, data + LFG_K, [](u32 x) {
return (Common::swap32(x) & 0x00C00000) == (Common::swap32(x) >> 2 & 0x00C00000);
}))
{
return false;
}
const size_t data_offset_mod_k = data_offset % LFG_K;
const size_t data_offset_div_k = data_offset / LFG_K;
std::copy(data, data + LFG_K - data_offset_mod_k, lfg->m_buffer.data() + data_offset_mod_k);
std::copy(data + LFG_K - data_offset_mod_k, data + LFG_K, lfg->m_buffer.data());
lfg->Backward(0, data_offset_mod_k);
for (size_t i = 0; i < data_offset_div_k; ++i)
lfg->Backward();
if (!lfg->Reinitialize(seed_out))
return false;
for (size_t i = 0; i < data_offset_div_k; ++i)
lfg->Forward();
return true;
}
void LaggedFibonacciGenerator::GetBytes(size_t count, u8* out)
{
while (count > 0)
{
const size_t length = std::min(count, LFG_K * sizeof(u32) - m_position_bytes);
std::memcpy(out, reinterpret_cast<u8*>(m_buffer.data()) + m_position_bytes, length);
m_position_bytes += length;
count -= length;
out += length;
if (m_position_bytes == LFG_K * sizeof(u32))
{
Forward();
m_position_bytes = 0;
}
}
}
u8 LaggedFibonacciGenerator::GetByte()
{
const u8 result = reinterpret_cast<u8*>(m_buffer.data())[m_position_bytes];
++m_position_bytes;
if (m_position_bytes == LFG_K * sizeof(u32))
{
Forward();
m_position_bytes = 0;
}
return result;
}
void LaggedFibonacciGenerator::Forward(size_t count)
{
m_position_bytes += count;
while (m_position_bytes >= LFG_K * sizeof(u32))
{
Forward();
m_position_bytes -= LFG_K * sizeof(u32);
}
}
void LaggedFibonacciGenerator::Forward()
{
for (size_t i = 0; i < LFG_J; ++i)
m_buffer[i] ^= m_buffer[i + LFG_K - LFG_J];
for (size_t i = LFG_J; i < LFG_K; ++i)
m_buffer[i] ^= m_buffer[i - LFG_J];
}
void LaggedFibonacciGenerator::Backward(size_t start_word, size_t end_word)
{
const size_t loop_end = std::max(LFG_J, start_word);
for (size_t i = std::min(end_word, LFG_K); i > loop_end; --i)
m_buffer[i - 1] ^= m_buffer[i - 1 - LFG_J];
for (size_t i = std::min(end_word, LFG_J); i > start_word; --i)
m_buffer[i - 1] ^= m_buffer[i - 1 + LFG_K - LFG_J];
}
bool LaggedFibonacciGenerator::Reinitialize(u32 seed_out[SEED_SIZE])
{
for (size_t i = 0; i < 4; ++i)
Backward();
for (u32& x : m_buffer)
x = Common::swap32(x);
// Reconstruct the bits which are missing due to the output code shifting by 18 instead of 16.
// Unfortunately we can't reconstruct bits 16 and 17 (counting LSB as 0) for the first word,
// but the observable result (when shifting by 18 instead of 16) is not affected by this.
for (size_t i = 0; i < SEED_SIZE; ++i)
{
m_buffer[i] = (m_buffer[i] & 0xFF00FFFF) | (m_buffer[i] << 2 & 0x00FC0000) |
((m_buffer[i + 16] ^ m_buffer[i + 15]) << 9 & 0x00030000);
}
for (size_t i = 0; i < SEED_SIZE; ++i)
seed_out[i] = Common::swap32(m_buffer[i]);
return Initialize(true);
}
bool LaggedFibonacciGenerator::Initialize(bool check_existing_data)
{
for (size_t i = SEED_SIZE; i < LFG_K; ++i)
{
const u32 calculated = (m_buffer[i - 17] << 23) ^ (m_buffer[i - 16] >> 9) ^ m_buffer[i - 1];
if (check_existing_data)
{
const u32 actual = (m_buffer[i] & 0xFF00FFFF) | (m_buffer[i] << 2 & 0x00FC0000);
if ((calculated & 0xFFFCFFFF) != actual)
return false;
}
m_buffer[i] = calculated;
}
// Instead of doing the "shift by 18 instead of 16" oddity when actually outputting the data,
// we can do the shifting (and byteswapping) at this point to make the output code simpler.
for (u32& x : m_buffer)
x = Common::swap32((x & 0xFF00FFFF) | ((x >> 2) & 0x00FF0000));
for (size_t i = 0; i < 4; ++i)
Forward();
return true;
}
} // namespace DiscIO

View File

@ -0,0 +1,51 @@
// This file is under the public domain.
#pragma once
#include <array>
#include <cstddef>
#include "Common/CommonTypes.h"
namespace DiscIO
{
class LaggedFibonacciGenerator
{
public:
static constexpr size_t SEED_SIZE = 17;
// Reconstructs a seed and writes it to seed_out, then returns the number of bytes which can
// be reconstructed using that seed. Can return any number between 0 and size, inclusive.
// data - data_offset must be 4-byte aligned.
static size_t GetSeed(const u8* data, size_t size, size_t data_offset, u32 seed_out[SEED_SIZE]);
// SetSeed must be called before using the functions below
void SetSeed(const u32 seed[SEED_SIZE]);
void SetSeed(const u8 seed[SEED_SIZE * sizeof(u32)]);
// Outputs a number of bytes and advances the internal state by the same amount.
void GetBytes(size_t count, u8* out);
u8 GetByte();
// Advances the internal state like GetBytes, but without outputting data. O(N), like GetBytes.
void Forward(size_t count);
private:
static bool GetSeed(const u32* data, size_t size, size_t data_offset,
LaggedFibonacciGenerator* lfg, u32 seed_out[SEED_SIZE]);
void Forward();
void Backward(size_t start_word = 0, size_t end_word = LFG_K);
bool Reinitialize(u32 seed_out[SEED_SIZE]);
bool Initialize(bool check_existing_data);
static constexpr size_t LFG_K = 521;
static constexpr size_t LFG_J = 32;
std::array<u32, LFG_K> m_buffer;
size_t m_position_bytes = 0;
};
} // namespace DiscIO

View File

@ -34,6 +34,7 @@
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/DiscExtractor.h" #include "DiscIO/DiscExtractor.h"
#include "DiscIO/LaggedFibonacciGenerator.h"
#include "DiscIO/MultithreadedCompressor.h" #include "DiscIO/MultithreadedCompressor.h"
#include "DiscIO/Volume.h" #include "DiscIO/Volume.h"
#include "DiscIO/VolumeWii.h" #include "DiscIO/VolumeWii.h"
@ -192,10 +193,9 @@ bool WIAFileReader::Initialize(const std::string& path)
const u32 number_of_raw_data_entries = Common::swap32(m_header_2.number_of_raw_data_entries); const u32 number_of_raw_data_entries = Common::swap32(m_header_2.number_of_raw_data_entries);
m_raw_data_entries.resize(number_of_raw_data_entries); m_raw_data_entries.resize(number_of_raw_data_entries);
Chunk& raw_data_entries = Chunk& raw_data_entries = ReadCompressedData(Common::swap64(m_header_2.raw_data_entries_offset),
ReadCompressedData(Common::swap64(m_header_2.raw_data_entries_offset),
Common::swap32(m_header_2.raw_data_entries_size), Common::swap32(m_header_2.raw_data_entries_size),
number_of_raw_data_entries * sizeof(RawDataEntry), false); number_of_raw_data_entries * sizeof(RawDataEntry));
if (!raw_data_entries.ReadAll(&m_raw_data_entries)) if (!raw_data_entries.ReadAll(&m_raw_data_entries))
return false; return false;
@ -211,7 +211,7 @@ bool WIAFileReader::Initialize(const std::string& path)
m_group_entries.resize(number_of_group_entries); m_group_entries.resize(number_of_group_entries);
Chunk& group_entries = ReadCompressedData(Common::swap64(m_header_2.group_entries_offset), Chunk& group_entries = ReadCompressedData(Common::swap64(m_header_2.group_entries_offset),
Common::swap32(m_header_2.group_entries_size), Common::swap32(m_header_2.group_entries_size),
number_of_group_entries * sizeof(GroupEntry), false); number_of_group_entries * sizeof(GroupEntry));
if (!group_entries.ReadAll(&m_group_entries)) if (!group_entries.ReadAll(&m_group_entries))
return false; return false;
@ -444,8 +444,8 @@ bool WIAFileReader::ReadFromGroups(u64* offset, u64* size, u8** out_ptr, u64 chu
else else
{ {
const u64 group_offset_in_file = static_cast<u64>(Common::swap32(group.data_offset)) << 2; const u64 group_offset_in_file = static_cast<u64>(Common::swap32(group.data_offset)) << 2;
Chunk& chunk = Chunk& chunk = ReadCompressedData(group_offset_in_file, group_data_size, chunk_size,
ReadCompressedData(group_offset_in_file, group_data_size, chunk_size, exception_lists); exception_lists, m_rvz, group_offset_in_data);
if (!chunk.Read(offset_in_group, bytes_to_read, *out_ptr)) if (!chunk.Read(offset_in_group, bytes_to_read, *out_ptr))
{ {
m_cached_chunk_offset = std::numeric_limits<u64>::max(); // Invalidate the cache m_cached_chunk_offset = std::numeric_limits<u64>::max(); // Invalidate the cache
@ -472,7 +472,8 @@ bool WIAFileReader::ReadFromGroups(u64* offset, u64* size, u8** out_ptr, u64 chu
} }
WIAFileReader::Chunk& WIAFileReader::ReadCompressedData(u64 offset_in_file, u64 compressed_size, WIAFileReader::Chunk& WIAFileReader::ReadCompressedData(u64 offset_in_file, u64 compressed_size,
u64 decompressed_size, u32 exception_lists) u64 decompressed_size, u32 exception_lists,
bool rvz_pack, u64 data_offset)
{ {
if (offset_in_file == m_cached_chunk_offset) if (offset_in_file == m_cached_chunk_offset)
return m_cached_chunk; return m_cached_chunk;
@ -504,8 +505,9 @@ WIAFileReader::Chunk& WIAFileReader::ReadCompressedData(u64 offset_in_file, u64
const bool compressed_exception_lists = m_compression_type > WIACompressionType::Purge; const bool compressed_exception_lists = m_compression_type > WIACompressionType::Purge;
m_cached_chunk = Chunk(&m_file, offset_in_file, compressed_size, decompressed_size, m_cached_chunk =
exception_lists, compressed_exception_lists, std::move(decompressor)); Chunk(&m_file, offset_in_file, compressed_size, decompressed_size, exception_lists,
compressed_exception_lists, rvz_pack, data_offset, std::move(decompressor));
m_cached_chunk_offset = offset_in_file; m_cached_chunk_offset = offset_in_file;
return m_cached_chunk; return m_cached_chunk;
} }
@ -793,6 +795,135 @@ bool WIAFileReader::ZstdDecompressor::Decompress(const DecompressionBuffer& in,
return !ZSTD_isError(result); return !ZSTD_isError(result);
} }
WIAFileReader::RVZPackDecompressor::RVZPackDecompressor(std::unique_ptr<Decompressor> decompressor,
DecompressionBuffer decompressed,
u64 data_offset)
: m_decompressor(std::move(decompressor)), m_decompressed(std::move(decompressed)),
m_data_offset(data_offset)
{
}
std::optional<bool> WIAFileReader::RVZPackDecompressor::ReadToDecompressed(
const DecompressionBuffer& in, size_t* in_bytes_read, size_t decompressed_bytes_read,
size_t bytes_to_read)
{
if (m_decompressed.data.size() < decompressed_bytes_read + bytes_to_read)
m_decompressed.data.resize(decompressed_bytes_read + bytes_to_read);
if (m_decompressed.bytes_written < decompressed_bytes_read + bytes_to_read)
{
if (!m_decompressor->Decompress(in, &m_decompressed, in_bytes_read))
return false;
if (m_decompressed.bytes_written < decompressed_bytes_read + bytes_to_read)
return true;
}
return std::nullopt;
}
bool WIAFileReader::RVZPackDecompressor::Decompress(const DecompressionBuffer& in,
DecompressionBuffer* out, size_t* in_bytes_read)
{
while (out->data.size() != out->bytes_written && !Done())
{
if (m_size == 0)
{
if (m_decompressed.bytes_written == m_decompressed_bytes_read)
{
m_decompressed.data.resize(sizeof(u32));
m_decompressed.bytes_written = 0;
m_decompressed_bytes_read = 0;
}
std::optional<bool> result =
ReadToDecompressed(in, in_bytes_read, m_decompressed_bytes_read, sizeof(u32));
if (result)
return *result;
m_size = Common::swap32(m_decompressed.data.data() + m_decompressed_bytes_read);
m_junk = m_size & 0x80000000;
if (m_junk)
{
m_size &= 0x7FFFFFFF;
constexpr size_t SEED_SIZE = LaggedFibonacciGenerator::SEED_SIZE * sizeof(u32);
result = ReadToDecompressed(in, in_bytes_read, m_decompressed_bytes_read + sizeof(u32),
SEED_SIZE);
if (result)
return *result;
m_lfg.SetSeed(m_decompressed.data.data() + m_decompressed_bytes_read + sizeof(u32));
m_lfg.Forward(m_data_offset % VolumeWii::BLOCK_TOTAL_SIZE);
m_decompressed_bytes_read += SEED_SIZE;
}
m_decompressed_bytes_read += sizeof(u32);
}
size_t bytes_to_write = std::min<size_t>(m_size, out->data.size() - out->bytes_written);
if (m_junk)
{
m_lfg.GetBytes(bytes_to_write, out->data.data() + out->bytes_written);
out->bytes_written += bytes_to_write;
}
else
{
if (m_decompressed.bytes_written != m_decompressed_bytes_read)
{
bytes_to_write =
std::min(bytes_to_write, m_decompressed.bytes_written - m_decompressed_bytes_read);
std::memcpy(out->data.data() + out->bytes_written,
m_decompressed.data.data() + m_decompressed_bytes_read, bytes_to_write);
m_decompressed_bytes_read += bytes_to_write;
out->bytes_written += bytes_to_write;
}
else
{
const size_t prev_out_bytes_written = out->bytes_written;
const size_t old_out_size = out->data.size();
const size_t new_out_size = out->bytes_written + bytes_to_write;
if (new_out_size < old_out_size)
out->data.resize(new_out_size);
if (!m_decompressor->Decompress(in, out, in_bytes_read))
return false;
out->data.resize(old_out_size);
bytes_to_write = out->bytes_written - prev_out_bytes_written;
if (bytes_to_write == 0)
return true;
}
}
m_data_offset += bytes_to_write;
m_size -= static_cast<u32>(bytes_to_write);
}
// If out is full but not all data has been read from in, give the decompressor a chance to read
// from in anyway. This is needed for the case where zstd has read everything except the checksum.
if (out->data.size() == out->bytes_written && in.bytes_written != *in_bytes_read)
{
if (!m_decompressor->Decompress(in, out, in_bytes_read))
return false;
}
return true;
}
bool WIAFileReader::RVZPackDecompressor::Done() const
{
return m_size == 0 && m_decompressed.bytes_written == m_decompressed_bytes_read &&
m_decompressor->Done();
}
WIAFileReader::Compressor::~Compressor() = default; WIAFileReader::Compressor::~Compressor() = default;
WIAFileReader::PurgeCompressor::PurgeCompressor() WIAFileReader::PurgeCompressor::PurgeCompressor()
@ -1169,11 +1300,11 @@ WIAFileReader::Chunk::Chunk() = default;
WIAFileReader::Chunk::Chunk(File::IOFile* file, u64 offset_in_file, u64 compressed_size, WIAFileReader::Chunk::Chunk(File::IOFile* file, u64 offset_in_file, u64 compressed_size,
u64 decompressed_size, u32 exception_lists, u64 decompressed_size, u32 exception_lists,
bool compressed_exception_lists, bool compressed_exception_lists, bool rvz_pack, u64 data_offset,
std::unique_ptr<Decompressor> decompressor) std::unique_ptr<Decompressor> decompressor)
: m_file(file), m_offset_in_file(offset_in_file), m_exception_lists(exception_lists), : m_file(file), m_offset_in_file(offset_in_file), m_exception_lists(exception_lists),
m_compressed_exception_lists(compressed_exception_lists), m_compressed_exception_lists(compressed_exception_lists), m_rvz_pack(rvz_pack),
m_decompressor(std::move(decompressor)) m_data_offset(data_offset), m_decompressor(std::move(decompressor))
{ {
constexpr size_t MAX_SIZE_PER_EXCEPTION_LIST = constexpr size_t MAX_SIZE_PER_EXCEPTION_LIST =
Common::AlignUp(VolumeWii::BLOCK_HEADER_SIZE, sizeof(SHA1)) / sizeof(SHA1) * Common::AlignUp(VolumeWii::BLOCK_HEADER_SIZE, sizeof(SHA1)) / sizeof(SHA1) *
@ -1250,7 +1381,7 @@ bool WIAFileReader::Chunk::Read(u64 offset, u64 size, u8* out_ptr)
if (m_exception_lists == 0 || m_compressed_exception_lists) if (m_exception_lists == 0 || m_compressed_exception_lists)
{ {
if (!m_decompressor->Decompress(m_in, &m_out, &m_in_bytes_read)) if (!Decompress())
return false; return false;
} }
@ -1261,6 +1392,12 @@ bool WIAFileReader::Chunk::Read(u64 offset, u64 size, u8* out_ptr)
{ {
return false; return false;
} }
if (m_rvz_pack && m_exception_lists == 0)
{
if (!Decompress())
return false;
}
} }
if (m_exception_lists == 0) if (m_exception_lists == 0)
@ -1289,6 +1426,26 @@ bool WIAFileReader::Chunk::Read(u64 offset, u64 size, u8* out_ptr)
return true; return true;
} }
bool WIAFileReader::Chunk::Decompress()
{
if (m_rvz_pack && m_exception_lists == 0)
{
m_rvz_pack = false;
const size_t bytes_to_move = m_out.bytes_written - m_out_bytes_used_for_exceptions;
DecompressionBuffer in{std::vector<u8>(bytes_to_move), bytes_to_move};
std::memcpy(in.data.data(), m_out.data.data() + m_out_bytes_used_for_exceptions, bytes_to_move);
m_out.bytes_written = m_out_bytes_used_for_exceptions;
m_decompressor = std::make_unique<RVZPackDecompressor>(std::move(m_decompressor), std::move(in),
m_data_offset);
}
return m_decompressor->Decompress(m_in, &m_out, &m_in_bytes_read);
}
bool WIAFileReader::Chunk::HandleExceptions(const u8* data, size_t bytes_allocated, bool WIAFileReader::Chunk::HandleExceptions(const u8* data, size_t bytes_allocated,
size_t bytes_written, size_t* bytes_used, bool align) size_t bytes_written, size_t* bytes_used, bool align)
{ {
@ -1633,23 +1790,120 @@ static bool AllSame(const u8* begin, const u8* end)
return AllAre(begin, end, *begin); return AllAre(begin, end, *begin);
}; };
ConversionResult<WIAFileReader::OutputParameters> void WIAFileReader::RVZPack(const u8* in, OutputParametersEntry* out, u64 bytes_per_chunk,
WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters parameters, size_t chunks, u64 total_size, u64 data_offset, u64 in_offset,
bool allow_junk_reuse)
{
using Seed = std::array<u32, LaggedFibonacciGenerator::SEED_SIZE>;
struct JunkInfo
{
size_t start_offset;
Seed seed;
};
// Maps end_offset -> (start_offset, seed)
std::map<size_t, JunkInfo> junk_info;
size_t position = 0;
while (position < total_size)
{
const size_t bytes_to_read =
std::min(Common::AlignUp(data_offset + 1, VolumeWii::BLOCK_TOTAL_SIZE) - data_offset,
total_size - position);
const size_t data_offset_mod = static_cast<size_t>(data_offset % VolumeWii::BLOCK_TOTAL_SIZE);
Seed seed;
const size_t bytes_reconstructed = LaggedFibonacciGenerator::GetSeed(
in + in_offset + position, bytes_to_read, data_offset_mod, seed.data());
if (bytes_reconstructed > 0)
junk_info.emplace(position + bytes_reconstructed, JunkInfo{position, seed});
position += bytes_to_read;
data_offset += bytes_to_read;
}
for (size_t i = 0; i < chunks; ++i)
{
OutputParametersEntry& entry = out[i];
if (entry.reused_group)
continue;
u64 current_offset = i * bytes_per_chunk;
const u64 end_offset = std::min(current_offset + bytes_per_chunk, total_size);
const bool store_junk_efficiently = allow_junk_reuse || !entry.reuse_id;
while (current_offset < end_offset)
{
constexpr size_t SEED_SIZE = LaggedFibonacciGenerator::SEED_SIZE * sizeof(u32);
u64 next_junk_start = end_offset;
u64 next_junk_end = end_offset;
Seed* seed = nullptr;
if (store_junk_efficiently && end_offset - current_offset > SEED_SIZE)
{
const auto next_junk_it = junk_info.upper_bound(current_offset + SEED_SIZE);
if (next_junk_it != junk_info.end() &&
next_junk_it->second.start_offset + SEED_SIZE < end_offset)
{
next_junk_start = std::max<u64>(current_offset, next_junk_it->second.start_offset);
next_junk_end = std::min<u64>(end_offset, next_junk_it->first);
seed = &next_junk_it->second.seed;
}
}
const u64 non_junk_bytes = next_junk_start - current_offset;
if (non_junk_bytes > 0)
{
const u8* ptr = in + in_offset + current_offset;
PushBack(&entry.main_data, Common::swap32(static_cast<u32>(non_junk_bytes)));
PushBack(&entry.main_data, ptr, ptr + non_junk_bytes);
current_offset += non_junk_bytes;
}
const u64 junk_bytes = next_junk_end - current_offset;
if (junk_bytes > 0)
{
PushBack(&entry.main_data, Common::swap32(static_cast<u32>(junk_bytes) | 0x80000000));
PushBack(&entry.main_data, *seed);
current_offset += junk_bytes;
}
}
}
}
void WIAFileReader::RVZPack(const u8* in, OutputParametersEntry* out, u64 size, u64 data_offset,
bool allow_junk_reuse)
{
RVZPack(in, out, size, 1, size, data_offset, 0, allow_junk_reuse);
}
ConversionResult<WIAFileReader::OutputParameters> WIAFileReader::ProcessAndCompress(
CompressThreadState* state, CompressParameters parameters,
const std::vector<PartitionEntry>& partition_entries, const std::vector<PartitionEntry>& partition_entries,
const std::vector<DataEntry>& data_entries, const std::vector<DataEntry>& data_entries, std::map<ReuseID, GroupEntry>* reusable_groups,
std::map<ReuseID, GroupEntry>* reusable_groups, std::mutex* reusable_groups_mutex, u64 chunks_per_wii_group, u64 exception_lists_per_chunk,
std::mutex* reusable_groups_mutex, u64 chunks_per_wii_group, bool compressed_exception_lists, bool rvz)
u64 exception_lists_per_chunk, bool compressed_exception_lists)
{ {
std::vector<OutputParametersEntry> output_entries; std::vector<OutputParametersEntry> output_entries;
if (!parameters.data_entry->is_partition) if (!parameters.data_entry->is_partition)
{ {
OutputParametersEntry& entry = output_entries.emplace_back(); OutputParametersEntry& entry = output_entries.emplace_back();
entry.main_data = std::move(parameters.data); std::vector<u8>& data = parameters.data;
if (AllSame(entry.main_data)) if (AllSame(data))
entry.reuse_id = ReuseID{nullptr, entry.main_data.size(), false, entry.main_data.front()}; entry.reuse_id = ReuseID{nullptr, data.size(), false, data.front()};
if (rvz)
RVZPack(data.data(), output_entries.data(), data.size(), parameters.data_offset, true);
else
entry.main_data = std::move(data);
} }
else else
{ {
@ -1676,9 +1930,9 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
const size_t first_chunk = output_entries.size(); const size_t first_chunk = output_entries.size();
const auto create_reuse_id = [&partition_entry, blocks, const auto create_reuse_id = [&partition_entry, blocks,
blocks_per_chunk](u8 value, bool decrypted, u64 block) { blocks_per_chunk](u8 value, bool encrypted, u64 block) {
const u64 size = std::min(blocks - block, blocks_per_chunk) * VolumeWii::BLOCK_DATA_SIZE; const u64 size = std::min(blocks - block, blocks_per_chunk) * VolumeWii::BLOCK_DATA_SIZE;
return ReuseID{&partition_entry.partition_key, size, decrypted, value}; return ReuseID{&partition_entry.partition_key, size, encrypted, value};
}; };
const u8* parameters_data_end = parameters.data.data() + parameters.data.size(); const u8* parameters_data_end = parameters.data.data() + parameters.data.size();
@ -1692,7 +1946,7 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
// Set this chunk as reusable if the encrypted data is AllSame // Set this chunk as reusable if the encrypted data is AllSame
const u8* data = parameters.data.data() + block_index * VolumeWii::BLOCK_TOTAL_SIZE; const u8* data = parameters.data.data() + block_index * VolumeWii::BLOCK_TOTAL_SIZE;
if (AllSame(data, std::min(parameters_data_end, data + in_data_per_chunk))) if (AllSame(data, std::min(parameters_data_end, data + in_data_per_chunk)))
reuse_id = create_reuse_id(parameters.data.front(), false, i * blocks_per_chunk); reuse_id = create_reuse_id(parameters.data.front(), true, i * blocks_per_chunk);
TryReuse(reusable_groups, reusable_groups_mutex, &entry); TryReuse(reusable_groups, reusable_groups_mutex, &entry);
if (!entry.reused_group && reuse_id) if (!entry.reused_group && reuse_id)
@ -1794,6 +2048,22 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
static_assert(std::is_trivially_copyable_v<decltype( static_assert(std::is_trivially_copyable_v<decltype(
CompressThreadState::decryption_buffer)::value_type>); CompressThreadState::decryption_buffer)::value_type>);
if (rvz)
{
// We must not store junk efficiently for chunks that may get reused at a position
// which has a different value of data_offset % VolumeWii::BLOCK_TOTAL_SIZE
const bool allow_junk_reuse = chunks_per_wii_group == 1;
const u64 bytes_per_chunk = std::min(out_data_per_chunk, VolumeWii::GROUP_DATA_SIZE);
const u64 total_size = blocks_in_this_group * VolumeWii::BLOCK_DATA_SIZE;
const u64 data_offset = parameters.data_offset + write_offset_of_group;
RVZPack(state->decryption_buffer[0].data(), output_entries.data() + first_chunk,
bytes_per_chunk, chunks, total_size, data_offset, write_offset_of_group,
allow_junk_reuse);
}
else
{
const u8* in_ptr = state->decryption_buffer[0].data(); const u8* in_ptr = state->decryption_buffer[0].data();
for (u64 j = 0; j < chunks; ++j) for (u64 j = 0; j < chunks; ++j)
{ {
@ -1816,11 +2086,11 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
if (i == 0 && !entry.reuse_id) if (i == 0 && !entry.reuse_id)
{ {
if (AllSame(in_ptr, in_ptr + bytes_to_write)) if (AllSame(in_ptr, in_ptr + bytes_to_write))
entry.reuse_id = create_reuse_id(*in_ptr, true, j * blocks_per_chunk); entry.reuse_id = create_reuse_id(*in_ptr, false, j * blocks_per_chunk);
} }
else else
{ {
if (entry.reuse_id && entry.reuse_id->decrypted && if (entry.reuse_id && !entry.reuse_id->encrypted &&
(!AllSame(in_ptr, in_ptr + bytes_to_write) || entry.reuse_id->value != *in_ptr)) (!AllSame(in_ptr, in_ptr + bytes_to_write) || entry.reuse_id->value != *in_ptr))
{ {
entry.reuse_id.reset(); entry.reuse_id.reset();
@ -1831,6 +2101,7 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
in_ptr += out_data_per_chunk; in_ptr += out_data_per_chunk;
} }
} }
}
for (size_t i = 0; i < exception_lists.size(); ++i) for (size_t i = 0; i < exception_lists.size(); ++i)
{ {
@ -1853,7 +2124,7 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
// If this chunk was set as reusable because the decrypted data is AllSame, // If this chunk was set as reusable because the decrypted data is AllSame,
// but it has exceptions, unmark it as reusable // but it has exceptions, unmark it as reusable
if (entry.reuse_id && entry.reuse_id->decrypted && !AllZero(entry.exception_lists)) if (entry.reuse_id && !entry.reuse_id->encrypted && !AllZero(entry.exception_lists))
entry.reuse_id.reset(); entry.reuse_id.reset();
} }
} }
@ -1866,7 +2137,7 @@ WIAFileReader::ProcessAndCompress(CompressThreadState* state, CompressParameters
continue; continue;
// Special case - a compressed size of zero is treated by WIA as meaning the data is all zeroes // Special case - a compressed size of zero is treated by WIA as meaning the data is all zeroes
if (AllZero(entry.exception_lists) && AllZero(entry.main_data)) if (entry.reuse_id && !entry.reuse_id->encrypted && entry.reuse_id->value == 0)
{ {
entry.exception_lists.clear(); entry.exception_lists.clear();
entry.main_data.clear(); entry.main_data.clear();
@ -2084,7 +2355,7 @@ WIAFileReader::ConvertToWIA(BlobReader* infile, const VolumeDisc* infile_volume,
const auto process_and_compress = [&](CompressThreadState* state, CompressParameters parameters) { const auto process_and_compress = [&](CompressThreadState* state, CompressParameters parameters) {
return ProcessAndCompress(state, std::move(parameters), partition_entries, data_entries, return ProcessAndCompress(state, std::move(parameters), partition_entries, data_entries,
&reusable_groups, &reusable_groups_mutex, chunks_per_wii_group, &reusable_groups, &reusable_groups_mutex, chunks_per_wii_group,
exception_lists_per_chunk, compressed_exception_lists); exception_lists_per_chunk, compressed_exception_lists, rvz);
}; };
const auto output = [&](OutputParameters parameters) { const auto output = [&](OutputParameters parameters) {
@ -2110,6 +2381,8 @@ WIAFileReader::ConvertToWIA(BlobReader* infile, const VolumeDisc* infile_volume,
u64 data_offset; u64 data_offset;
u64 data_size; u64 data_size;
u64 data_offset_in_partition;
if (data_entry.is_partition) if (data_entry.is_partition)
{ {
const PartitionEntry& partition_entry = partition_entries[data_entry.index]; const PartitionEntry& partition_entry = partition_entries[data_entry.index];
@ -2119,9 +2392,14 @@ WIAFileReader::ConvertToWIA(BlobReader* infile, const VolumeDisc* infile_volume,
first_group = Common::swap32(partition_data_entry.group_index); first_group = Common::swap32(partition_data_entry.group_index);
last_group = first_group + Common::swap32(partition_data_entry.number_of_groups); last_group = first_group + Common::swap32(partition_data_entry.number_of_groups);
data_offset = Common::swap32(partition_data_entry.first_sector) * VolumeWii::BLOCK_TOTAL_SIZE; const u32 first_sector = Common::swap32(partition_data_entry.first_sector);
data_offset = first_sector * VolumeWii::BLOCK_TOTAL_SIZE;
data_size = data_size =
Common::swap32(partition_data_entry.number_of_sectors) * VolumeWii::BLOCK_TOTAL_SIZE; Common::swap32(partition_data_entry.number_of_sectors) * VolumeWii::BLOCK_TOTAL_SIZE;
const u32 block_in_partition =
first_sector - Common::swap32(partition_entry.data_entries[0].first_sector);
data_offset_in_partition = block_in_partition * VolumeWii::BLOCK_DATA_SIZE;
} }
else else
{ {
@ -2136,6 +2414,8 @@ WIAFileReader::ConvertToWIA(BlobReader* infile, const VolumeDisc* infile_volume,
const u64 skipped_data = data_offset % VolumeWii::BLOCK_TOTAL_SIZE; const u64 skipped_data = data_offset % VolumeWii::BLOCK_TOTAL_SIZE;
data_offset -= skipped_data; data_offset -= skipped_data;
data_size += skipped_data; data_size += skipped_data;
data_offset_in_partition = data_offset;
} }
ASSERT(groups_processed == first_group); ASSERT(groups_processed == first_group);
@ -2157,11 +2437,26 @@ WIAFileReader::ConvertToWIA(BlobReader* infile, const VolumeDisc* infile_volume,
return ConversionResultCode::ReadFailed; return ConversionResultCode::ReadFailed;
bytes_read += bytes_to_read; bytes_read += bytes_to_read;
mt_compressor.CompressAndWrite( mt_compressor.CompressAndWrite(CompressParameters{
CompressParameters{buffer, &data_entry, bytes_read, groups_processed}); buffer, &data_entry, data_offset_in_partition, bytes_read, groups_processed});
data_offset += bytes_to_read;
data_size -= bytes_to_read;
if (data_entry.is_partition)
{
data_offset_in_partition +=
bytes_to_read / VolumeWii::BLOCK_TOTAL_SIZE * VolumeWii::BLOCK_DATA_SIZE;
}
else
{
data_offset_in_partition += bytes_to_read;
}
groups_processed += Common::AlignUp(bytes_to_read, chunk_size) / chunk_size; groups_processed += Common::AlignUp(bytes_to_read, chunk_size) / chunk_size;
} }
ASSERT(data_size == 0);
} }
ASSERT(groups_processed == total_groups); ASSERT(groups_processed == total_groups);

View File

@ -10,6 +10,7 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <type_traits>
#include <utility> #include <utility>
#include <bzlib.h> #include <bzlib.h>
@ -21,6 +22,7 @@
#include "Common/File.h" #include "Common/File.h"
#include "Common/Swap.h" #include "Common/Swap.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/LaggedFibonacciGenerator.h"
#include "DiscIO/MultithreadedCompressor.h" #include "DiscIO/MultithreadedCompressor.h"
#include "DiscIO/WiiEncryptionCache.h" #include "DiscIO/WiiEncryptionCache.h"
@ -267,6 +269,31 @@ private:
ZSTD_DStream* m_stream; ZSTD_DStream* m_stream;
}; };
class RVZPackDecompressor final : public Decompressor
{
public:
RVZPackDecompressor(std::unique_ptr<Decompressor> decompressor,
DecompressionBuffer decompressed, u64 data_offset);
bool Decompress(const DecompressionBuffer& in, DecompressionBuffer* out,
size_t* in_bytes_read) override;
bool Done() const override;
private:
std::optional<bool> ReadToDecompressed(const DecompressionBuffer& in, size_t* in_bytes_read,
size_t decompressed_bytes_read, size_t bytes_to_read);
std::unique_ptr<Decompressor> m_decompressor;
DecompressionBuffer m_decompressed;
size_t m_decompressed_bytes_read = 0;
u64 m_data_offset;
u32 m_size = 0;
bool m_junk;
LaggedFibonacciGenerator m_lfg;
};
class Compressor class Compressor
{ {
public: public:
@ -375,7 +402,7 @@ private:
public: public:
Chunk(); Chunk();
Chunk(File::IOFile* file, u64 offset_in_file, u64 compressed_size, u64 decompressed_size, Chunk(File::IOFile* file, u64 offset_in_file, u64 compressed_size, u64 decompressed_size,
u32 exception_lists, bool compressed_exception_lists, u32 exception_lists, bool compressed_exception_lists, bool rvz_pack, u64 data_offset,
std::unique_ptr<Decompressor> decompressor); std::unique_ptr<Decompressor> decompressor);
bool Read(u64 offset, u64 size, u8* out_ptr); bool Read(u64 offset, u64 size, u8* out_ptr);
@ -391,6 +418,7 @@ private:
} }
private: private:
bool Decompress();
bool HandleExceptions(const u8* data, size_t bytes_allocated, size_t bytes_written, bool HandleExceptions(const u8* data, size_t bytes_allocated, size_t bytes_written,
size_t* bytes_used, bool align); size_t* bytes_used, bool align);
@ -407,6 +435,8 @@ private:
size_t m_in_bytes_used_for_exceptions = 0; size_t m_in_bytes_used_for_exceptions = 0;
u32 m_exception_lists = 0; u32 m_exception_lists = 0;
bool m_compressed_exception_lists = false; bool m_compressed_exception_lists = false;
bool m_rvz_pack = false;
u64 m_data_offset = 0;
}; };
explicit WIAFileReader(File::IOFile file, const std::string& path); explicit WIAFileReader(File::IOFile file, const std::string& path);
@ -417,7 +447,7 @@ private:
u64 data_offset, u64 data_size, u32 group_index, u32 number_of_groups, u64 data_offset, u64 data_size, u32 group_index, u32 number_of_groups,
u32 exception_lists); u32 exception_lists);
Chunk& ReadCompressedData(u64 offset_in_file, u64 compressed_size, u64 decompressed_size, Chunk& ReadCompressedData(u64 offset_in_file, u64 compressed_size, u64 decompressed_size,
u32 exception_lists); u32 exception_lists = 0, bool rvz_pack = false, u64 data_offset = 0);
static bool ApplyHashExceptions(const std::vector<HashExceptionEntry>& exception_list, static bool ApplyHashExceptions(const std::vector<HashExceptionEntry>& exception_list,
VolumeWii::HashBlock hash_blocks[VolumeWii::BLOCKS_PER_GROUP]); VolumeWii::HashBlock hash_blocks[VolumeWii::BLOCKS_PER_GROUP]);
@ -430,18 +460,18 @@ private:
{ {
bool operator==(const ReuseID& other) const bool operator==(const ReuseID& other) const
{ {
return std::tie(partition_key, data_size, decrypted, value) == return std::tie(partition_key, data_size, encrypted, value) ==
std::tie(other.partition_key, other.data_size, other.decrypted, other.value); std::tie(other.partition_key, other.data_size, other.encrypted, other.value);
} }
bool operator<(const ReuseID& other) const bool operator<(const ReuseID& other) const
{ {
return std::tie(partition_key, data_size, decrypted, value) < return std::tie(partition_key, data_size, encrypted, value) <
std::tie(other.partition_key, other.data_size, other.decrypted, other.value); std::tie(other.partition_key, other.data_size, other.encrypted, other.value);
} }
bool operator>(const ReuseID& other) const bool operator>(const ReuseID& other) const
{ {
return std::tie(partition_key, data_size, decrypted, value) > return std::tie(partition_key, data_size, encrypted, value) >
std::tie(other.partition_key, other.data_size, other.decrypted, other.value); std::tie(other.partition_key, other.data_size, other.encrypted, other.value);
} }
bool operator!=(const ReuseID& other) const { return !operator==(other); } bool operator!=(const ReuseID& other) const { return !operator==(other); }
bool operator>=(const ReuseID& other) const { return !operator<(other); } bool operator>=(const ReuseID& other) const { return !operator<(other); }
@ -449,7 +479,7 @@ private:
const WiiKey* partition_key; const WiiKey* partition_key;
u64 data_size; u64 data_size;
bool decrypted; bool encrypted;
u8 value; u8 value;
}; };
@ -470,6 +500,7 @@ private:
{ {
std::vector<u8> data; std::vector<u8> data;
const DataEntry* data_entry; const DataEntry* data_entry;
u64 data_offset;
u64 bytes_read; u64 bytes_read;
size_t group_index; size_t group_index;
}; };
@ -512,13 +543,17 @@ private:
WIAHeader2* header_2); WIAHeader2* header_2);
static bool TryReuse(std::map<ReuseID, GroupEntry>* reusable_groups, static bool TryReuse(std::map<ReuseID, GroupEntry>* reusable_groups,
std::mutex* reusable_groups_mutex, OutputParametersEntry* entry); std::mutex* reusable_groups_mutex, OutputParametersEntry* entry);
static void RVZPack(const u8* in, OutputParametersEntry* out, u64 bytes_per_chunk, size_t chunks,
u64 total_size, u64 data_offset, u64 in_offset, bool allow_junk_reuse);
static void RVZPack(const u8* in, OutputParametersEntry* out, u64 size, u64 data_offset,
bool allow_junk_reuse);
static ConversionResult<OutputParameters> static ConversionResult<OutputParameters>
ProcessAndCompress(CompressThreadState* state, CompressParameters parameters, ProcessAndCompress(CompressThreadState* state, CompressParameters parameters,
const std::vector<PartitionEntry>& partition_entries, const std::vector<PartitionEntry>& partition_entries,
const std::vector<DataEntry>& data_entries, const std::vector<DataEntry>& data_entries,
std::map<ReuseID, GroupEntry>* reusable_groups, std::map<ReuseID, GroupEntry>* reusable_groups,
std::mutex* reusable_groups_mutex, u64 chunks_per_wii_group, std::mutex* reusable_groups_mutex, u64 chunks_per_wii_group,
u64 exception_lists_per_chunk, bool compressed_exception_lists); u64 exception_lists_per_chunk, bool compressed_exception_lists, bool rvz);
static ConversionResultCode Output(std::vector<OutputParametersEntry>* entries, static ConversionResultCode Output(std::vector<OutputParametersEntry>* entries,
File::IOFile* outfile, File::IOFile* outfile,
std::map<ReuseID, GroupEntry>* reusable_groups, std::map<ReuseID, GroupEntry>* reusable_groups,
@ -528,13 +563,20 @@ private:
u32 total_groups, u64 iso_size, CompressCB callback, u32 total_groups, u64 iso_size, CompressCB callback,
void* arg); void* arg);
static void PushBack(std::vector<u8>* vector, const u8* begin, const u8* end)
{
const size_t offset_in_vector = vector->size();
vector->resize(offset_in_vector + (end - begin));
std::copy(begin, end, vector->data() + offset_in_vector);
}
template <typename T> template <typename T>
static void PushBack(std::vector<u8>* vector, const T& x) static void PushBack(std::vector<u8>* vector, const T& x)
{ {
const size_t offset_in_vector = vector->size(); static_assert(std::is_trivially_copyable_v<T>);
vector->resize(offset_in_vector + sizeof(T));
const u8* x_ptr = reinterpret_cast<const u8*>(&x); const u8* x_ptr = reinterpret_cast<const u8*>(&x);
std::copy(x_ptr, x_ptr + sizeof(T), vector->data() + offset_in_vector); PushBack(vector, x_ptr, x_ptr + sizeof(T));
} }
bool m_valid; bool m_valid;
@ -566,9 +608,9 @@ private:
static constexpr u32 WIA_VERSION_WRITE_COMPATIBLE = 0x01000000; static constexpr u32 WIA_VERSION_WRITE_COMPATIBLE = 0x01000000;
static constexpr u32 WIA_VERSION_READ_COMPATIBLE = 0x00080000; static constexpr u32 WIA_VERSION_READ_COMPATIBLE = 0x00080000;
static constexpr u32 RVZ_VERSION = 0x00010000; static constexpr u32 RVZ_VERSION = 0x00020000;
static constexpr u32 RVZ_VERSION_WRITE_COMPATIBLE = 0x00010000; static constexpr u32 RVZ_VERSION_WRITE_COMPATIBLE = 0x00020000;
static constexpr u32 RVZ_VERSION_READ_COMPATIBLE = 0x00010000; static constexpr u32 RVZ_VERSION_READ_COMPATIBLE = 0x00020000;
}; };
} // namespace DiscIO } // namespace DiscIO

View File

@ -252,6 +252,10 @@ void ConvertDialog::OnFormatChanged()
m_block_size->setEnabled(m_block_size->count() > 1); m_block_size->setEnabled(m_block_size->count() > 1);
m_compression->setEnabled(m_compression->count() > 1); m_compression->setEnabled(m_compression->count() > 1);
m_scrub->setEnabled(format != DiscIO::BlobType::RVZ);
if (format == DiscIO::BlobType::RVZ)
m_scrub->setChecked(false);
} }
void ConvertDialog::OnCompressionChanged() void ConvertDialog::OnCompressionChanged()