diff options
| author | Stefan Boberg <[email protected]> | 2023-05-23 10:13:47 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-05-23 10:13:47 +0200 |
| commit | 633dee5f8688c104a04f0ec719b756dbbad7142f (patch) | |
| tree | f0ce192964f6d6b9fd506f4963e261320803e2ac /src | |
| parent | MemoryView::RightChop -> const (diff) | |
| download | zen-633dee5f8688c104a04f0ec719b756dbbad7142f.tar.xz zen-633dee5f8688c104a04f0ec719b756dbbad7142f.zip | |
streaming decompression support (#142)
Added CompressedBufferReader support from UE. This provides some streaming decompression support which can be employed to reduce memory and other resource usage.
Diffstat (limited to 'src')
| -rw-r--r-- | src/zencore/compositebuffer.cpp | 13 | ||||
| -rw-r--r-- | src/zencore/compress.cpp | 806 | ||||
| -rw-r--r-- | src/zencore/include/zencore/compositebuffer.h | 14 | ||||
| -rw-r--r-- | src/zencore/include/zencore/compress.h | 118 | ||||
| -rw-r--r-- | src/zencore/include/zencore/intmath.h | 26 | ||||
| -rw-r--r-- | src/zencore/include/zencore/stream.h | 27 | ||||
| -rw-r--r-- | src/zencore/include/zencore/zencore.h | 4 | ||||
| -rw-r--r-- | src/zencore/stream.cpp | 21 |
8 files changed, 1000 insertions, 29 deletions
diff --git a/src/zencore/compositebuffer.cpp b/src/zencore/compositebuffer.cpp index 735020451..c49bf775c 100644 --- a/src/zencore/compositebuffer.cpp +++ b/src/zencore/compositebuffer.cpp @@ -58,7 +58,7 @@ CompositeBuffer::MakeOwned() && } SharedBuffer -CompositeBuffer::Flatten() const& +CompositeBuffer::ToShared() const& { switch (m_Segments.size()) { @@ -81,7 +81,7 @@ CompositeBuffer::Flatten() const& } SharedBuffer -CompositeBuffer::Flatten() && +CompositeBuffer::ToShared() && { return m_Segments.size() == 1 ? std::move(m_Segments[0]) : std::as_const(*this).Flatten(); } @@ -100,10 +100,13 @@ CompositeBuffer::Mid(uint64_t Offset, uint64_t Size) const } MemoryView -CompositeBuffer::ViewOrCopyRange(uint64_t Offset, uint64_t Size, UniqueBuffer& CopyBuffer) const +CompositeBuffer::ViewOrCopyRange(uint64_t Offset, + uint64_t Size, + UniqueBuffer& CopyBuffer, + std::function<UniqueBuffer(uint64_t Size)> Allocator) const { MemoryView View; - IterateRange(Offset, Size, [Size, &View, &CopyBuffer, WriteView = MutableMemoryView()](MemoryView Segment) mutable { + IterateRange(Offset, Size, [Size, &View, &CopyBuffer, &Allocator, WriteView = MutableMemoryView()](MemoryView Segment) mutable { if (Size == Segment.GetSize()) { View = Segment; @@ -114,7 +117,7 @@ CompositeBuffer::ViewOrCopyRange(uint64_t Offset, uint64_t Size, UniqueBuffer& C { if (CopyBuffer.GetSize() < Size) { - CopyBuffer = UniqueBuffer::Alloc(Size); + CopyBuffer = Allocator(Size); } View = WriteView = CopyBuffer.GetMutableView().Left(Size); } diff --git a/src/zencore/compress.cpp b/src/zencore/compress.cpp index 44d6efc36..2362d8e78 100644 --- a/src/zencore/compress.cpp +++ b/src/zencore/compress.cpp @@ -6,7 +6,9 @@ #include <zencore/compositebuffer.h> #include <zencore/crc32.h> #include <zencore/endian.h> +#include <zencore/intmath.h> #include <zencore/iohash.h> +#include <zencore/stream.h> #include <zencore/testing.h> #include <oodle2.h> @@ -19,7 +21,8 @@ namespace zen::detail { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -static constexpr uint64_t DefaultBlockSize = 256 * 1024; +static constexpr uint64_t DefaultBlockSize = 256 * 1024; +static constexpr uint64_t DefaultHeaderSize = 4 * 1024; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -115,27 +118,72 @@ struct BufferHeader } return Crc32; } + + bool TryGetCompressParameters(OodleCompressor& OutCompressor, OodleCompressionLevel& OutCompressionLevel, uint64_t& OutBlockSize) const + { + switch (Method) + { + case CompressionMethod::None: + OutCompressor = OodleCompressor::NotSet; + OutCompressionLevel = OodleCompressionLevel::None; + OutBlockSize = 0; + return true; + case CompressionMethod::Oodle: + OutCompressor = OodleCompressor(Compressor); + OutCompressionLevel = OodleCompressionLevel(CompressionLevel); + OutBlockSize = uint64_t(1) << BlockSizeExponent; + return true; + default: + return false; + } + } }; +using FHeader = BufferHeader; + static_assert(sizeof(BufferHeader) == 64, "BufferHeader is the wrong size."); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +class DecoderSource +{ +public: + virtual bool Read(uint64_t Offset, MutableMemoryView Data) const = 0; + virtual MemoryView ReadOrView(uint64_t Offset, uint64_t Size, DecoderContext& Context) const = 0; + virtual CompositeBuffer ReadToComposite(uint64_t Offset, uint64_t Size) const = 0; +}; + class BaseEncoder { public: - virtual CompositeBuffer Compress(const CompositeBuffer& RawData, uint64_t BlockSize = DefaultBlockSize) const = 0; + [[nodiscard]] virtual CompositeBuffer Compress(const CompositeBuffer& RawData, uint64_t BlockSize = DefaultBlockSize) const = 0; }; class BaseDecoder { public: - virtual CompositeBuffer Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const = 0; - virtual bool TryDecompressTo(const BufferHeader& Header, - const CompositeBuffer& CompressedData, - MutableMemoryView RawView, - uint64_t RawOffset) const = 0; - virtual uint64_t GetHeaderSize(const BufferHeader& Header) const = 0; + [[nodiscard]] virtual uint64_t GetHeaderSize(const BufferHeader& Header) const = 0; + [[nodiscard]] virtual bool TryDecompressTo(const BufferHeader& Header, + const CompositeBuffer& CompressedData, + MutableMemoryView RawView, + uint64_t RawOffset) const = 0; + + [[nodiscard]] virtual bool TryDecompressTo(DecoderContext& Context, + const DecoderSource& Source, + const BufferHeader& Header, + MemoryView HeaderView, + uint64_t RawOffset, + MutableMemoryView RawView) const = 0; + + [[nodiscard]] virtual CompositeBuffer DecompressToComposite(const BufferHeader& Header, + const CompositeBuffer& CompressedData) const = 0; + + virtual CompositeBuffer DecompressToComposite(DecoderContext& Context, + const DecoderSource& Source, + const FHeader& Header, + const MemoryView HeaderView, + uint64_t RawOffset, + uint64_t RawSize) const = 0; }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -161,7 +209,7 @@ public: class NoneDecoder final : public BaseDecoder { public: - [[nodiscard]] CompositeBuffer Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const final + [[nodiscard]] CompositeBuffer DecompressToComposite(const BufferHeader& Header, const CompositeBuffer& CompressedData) const final { if (Header.Method == CompressionMethod::None && Header.TotalCompressedSize == CompressedData.GetSize() && Header.TotalCompressedSize == Header.TotalRawSize + sizeof(BufferHeader)) @@ -171,6 +219,23 @@ public: return CompositeBuffer(); } + CompositeBuffer DecompressToComposite(DecoderContext& Context, + const DecoderSource& Source, + const FHeader& Header, + const MemoryView HeaderView, + const uint64_t RawOffset, + const uint64_t RawSize) const final + { + ZEN_UNUSED(HeaderView, Context); + + if (Header.Method == CompressionMethod::None && RawOffset <= Header.TotalRawSize && RawSize <= Header.TotalRawSize - RawOffset && + Header.TotalCompressedSize == Header.TotalRawSize + sizeof(FHeader)) + { + return Source.ReadToComposite(sizeof(FHeader) + RawOffset, RawSize); + } + return CompositeBuffer(); + } + [[nodiscard]] bool TryDecompressTo(const BufferHeader& Header, const CompositeBuffer& CompressedData, MutableMemoryView RawView, @@ -186,6 +251,24 @@ public: return false; } + [[nodiscard]] bool TryDecompressTo(DecoderContext& Context, + const DecoderSource& Source, + const BufferHeader& Header, + MemoryView HeaderView, + uint64_t RawOffset, + MutableMemoryView RawView) const final + { + ZEN_UNUSED(HeaderView, Context); + + if (Header.Method == CompressionMethod::None && RawOffset <= Header.TotalRawSize && + RawView.GetSize() <= Header.TotalRawSize - RawOffset && + Header.TotalCompressedSize == Header.TotalRawSize + sizeof(BufferHeader)) + { + return Source.Read(sizeof(BufferHeader) + RawOffset, RawView); + } + return false; + } + [[nodiscard]] uint64_t GetHeaderSize(const BufferHeader&) const final { return sizeof(BufferHeader); } }; @@ -305,22 +388,38 @@ BlockEncoder::Compress(const CompositeBuffer& RawData, const uint64_t BlockSize) class BlockDecoder : public BaseDecoder { public: - CompositeBuffer Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const final; - [[nodiscard]] bool TryDecompressTo(const BufferHeader& Header, - const CompositeBuffer& CompressedData, - MutableMemoryView RawView, - uint64_t RawOffset) const final; [[nodiscard]] uint64_t GetHeaderSize(const BufferHeader& Header) const final { return sizeof(BufferHeader) + sizeof(uint32_t) * uint64_t(Header.BlockCount); } + [[nodiscard]] virtual bool TryDecompressTo(DecoderContext& Context, + const DecoderSource& Source, + const BufferHeader& Header, + MemoryView HeaderView, + uint64_t RawOffset, + MutableMemoryView RawView) const final; + + CompositeBuffer DecompressToComposite(const BufferHeader& Header, const CompositeBuffer& CompressedData) const final; + + CompositeBuffer DecompressToComposite(DecoderContext& Context, + const DecoderSource& Source, + const FHeader& Header, + const MemoryView HeaderView, + const uint64_t RawOffset, + const uint64_t RawSize) const final; + + [[nodiscard]] bool TryDecompressTo(const BufferHeader& Header, + const CompositeBuffer& CompressedData, + MutableMemoryView RawView, + uint64_t RawOffset) const final; + protected: virtual bool DecompressBlock(MutableMemoryView RawData, MemoryView CompressedData) const = 0; }; CompositeBuffer -BlockDecoder::Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const +BlockDecoder::DecompressToComposite(const BufferHeader& Header, const CompositeBuffer& CompressedData) const { if (Header.BlockCount == 0 || Header.TotalCompressedSize != CompressedData.GetSize()) { @@ -519,6 +618,109 @@ BlockDecoder::TryDecompressTo(const BufferHeader& Header, return RemainingRawSize == 0; } +bool +BlockDecoder::TryDecompressTo(DecoderContext& Context, + const DecoderSource& Source, + const FHeader& Header, + const MemoryView HeaderView, + const uint64_t RawOffset, + MutableMemoryView RawView) const +{ + if (Header.TotalRawSize < RawOffset + RawView.GetSize()) + { + return false; + } + + const uint64_t BlockSize = uint64_t(1) << Header.BlockSizeExponent; + const uint64_t LastBlockSize = BlockSize - (BlockSize * Header.BlockCount - Header.TotalRawSize); + const uint32_t FirstBlockIndex = uint32_t(RawOffset / BlockSize); + const uint32_t LastBlockIndex = uint32_t((RawOffset + RawView.GetSize() - 1) / BlockSize); + + uint64_t RawBlockOffset = RawOffset % BlockSize; + + const uint32_t* const CompressedBlockSizes = static_cast<const uint32_t*>(HeaderView.RightChop(sizeof(FHeader)).GetData()); + uint64_t CompressedOffset = sizeof(FHeader) + sizeof(uint32_t) * uint32_t(Header.BlockCount); + + // + Algo::TransformAccumulate(MakeArrayView(CompressedBlockSizes, FirstBlockIndex), + // [](uint32_t Size) -> uint64_t { return NETWORK_ORDER32(Size); }, + // uint64_t(0) + + for (uint32_t i = 0; i < FirstBlockIndex; ++i) + { + CompressedOffset += ByteSwap(CompressedBlockSizes[i]); + } + + for (uint32_t BlockIndex = FirstBlockIndex; BlockIndex <= LastBlockIndex; BlockIndex++) + { + const uint64_t RawBlockSize = BlockIndex == Header.BlockCount - 1 ? LastBlockSize : BlockSize; + const uint64_t RawBlockReadSize = zen::Min(RawView.GetSize(), RawBlockSize - RawBlockOffset); + const uint32_t CompressedBlockSize = ByteSwap(CompressedBlockSizes[BlockIndex]); + const bool bIsCompressed = CompressedBlockSize < RawBlockSize; + + if (bIsCompressed) + { + if (Context.RawBlockIndex == BlockIndex) + { + RawView.Left(RawBlockReadSize).CopyFrom(Context.RawBlock.GetView().Mid(RawBlockOffset, RawBlockReadSize)); + } + else + { + MutableMemoryView RawBlock; + if (RawBlockReadSize == RawBlockSize) + { + RawBlock = RawView.Left(RawBlockSize); + } + else + { + if (Context.RawBlock.GetSize() < RawBlockSize) + { + Context.RawBlock = UniqueBuffer::Alloc(BlockSize); + } + RawBlock = Context.RawBlock.GetMutableView().Left(RawBlockSize); + Context.RawBlockIndex = BlockIndex; + } + + const MemoryView CompressedBlock = Source.ReadOrView(CompressedOffset, CompressedBlockSize, Context); + if (CompressedBlock.IsEmpty() || !DecompressBlock(RawBlock, CompressedBlock)) + { + return false; + } + + if (RawBlockReadSize != RawBlockSize) + { + RawView.CopyFrom(RawBlock.Mid(RawBlockOffset, RawBlockReadSize)); + } + } + } + else + { + Source.Read(CompressedOffset + RawBlockOffset, RawView.Left(RawBlockReadSize)); + } + + RawBlockOffset = 0; + CompressedOffset += CompressedBlockSize; + RawView += RawBlockReadSize; + } + + return RawView.GetSize() == 0; +} + +CompositeBuffer +BlockDecoder::DecompressToComposite(DecoderContext& Context, + const DecoderSource& Source, + const FHeader& Header, + const MemoryView HeaderView, + const uint64_t RawOffset, + const uint64_t RawSize) const +{ + UniqueBuffer Buffer = UniqueBuffer::Alloc(RawSize); + if (TryDecompressTo(Context, Source, Header, HeaderView, RawOffset, Buffer)) + { + return CompositeBuffer(Buffer.MoveToShared()); + } + return CompositeBuffer(); +} + ////////////////////////////////////////////////////////////////////////// struct OodleInit @@ -671,14 +873,15 @@ GetDecoder(CompressionMethod Method) bool BufferHeader::IsValid(const CompositeBuffer& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize) { - uint64_t Size = CompressedData.GetSize(); - if (Size < sizeof(BufferHeader)) + const uint64_t CompressedDataSize = CompressedData.GetSize(); + if (CompressedDataSize < sizeof(BufferHeader)) { return false; } + const size_t StackBufferSize = 256; uint8_t StackBuffer[StackBufferSize]; - uint64_t ReadSize = Min(Size, StackBufferSize); + uint64_t ReadSize = Min(CompressedDataSize, StackBufferSize); BufferHeader* Header = reinterpret_cast<BufferHeader*>(StackBuffer); { CompositeBuffer::Iterator It; @@ -689,20 +892,27 @@ BufferHeader::IsValid(const CompositeBuffer& CompressedData, IoHash& OutRawHash, { return false; } + const BaseDecoder* const Decoder = GetDecoder(Header->Method); if (!Decoder) { return false; } + uint32_t Crc32 = Header->Crc32; OutRawHash = IoHash::FromBLAKE3(Header->RawHash); OutRawSize = Header->TotalRawSize; uint64_t HeaderSize = Decoder->GetHeaderSize(*Header); + + if (Header->TotalCompressedSize > CompressedDataSize) + { + return false; + } + Header->ByteSwap(); if (HeaderSize > ReadSize) { - // 0.004% of cases on a Fortnite hot cache cook UniqueBuffer HeaderCopy = UniqueBuffer::Alloc(HeaderSize); CompositeBuffer::Iterator It; CompressedData.CopyTo(HeaderCopy.GetMutableView(), It); @@ -720,9 +930,170 @@ BufferHeader::IsValid(const CompositeBuffer& CompressedData, IoHash& OutRawHash, return false; } } + return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +static bool +TryReadHeader(DecoderContext& Context, Archive& Ar, FHeader& OutHeader, MemoryView& OutHeaderView) +{ + if (Context.HeaderOffset != MAX_uint64) + { + OutHeaderView = Context.Header.GetView().Left(Context.HeaderSize); + MakeMutableMemoryView(&OutHeader, &OutHeader + 1).CopyFrom(OutHeaderView.Left(sizeof(FHeader))); + OutHeader.ByteSwap(); + return true; + } + + const int64_t Offset = Ar.Tell(); + + FHeader& Header = OutHeader; + Ar.Serialize(&Header, sizeof(FHeader)); + Header.ByteSwap(); + + if (const BaseDecoder* const Decoder = GetDecoder(Header.Method); Decoder && Header.Magic == FHeader::ExpectedMagic) + { + const uint64_t HeaderSize = Decoder->GetHeaderSize(Header); + if (Context.Header.GetSize() < HeaderSize) + { + Context.Header = UniqueBuffer::Alloc(zen::Max(NextPow2(HeaderSize), DefaultHeaderSize)); + } + + const MutableMemoryView HeaderView = Context.Header.GetMutableView().Left(HeaderSize); + const MutableMemoryView HeaderTail = HeaderView.CopyFrom(MakeMemoryView(&Header, &Header + 1)); + Ar.Serialize(HeaderTail.GetData(), int64_t(HeaderTail.GetSize())); + + FHeader* const HeaderCopy = static_cast<FHeader*>(HeaderView.GetData()); + HeaderCopy->ByteSwap(); + if (Header.Crc32 == FHeader::CalculateCrc32(HeaderView)) + { + Context.HeaderOffset = uint64_t(Offset); + Context.HeaderSize = HeaderSize; + Context.HeaderCrc32 = Header.Crc32; + Context.RawBlockIndex = MAX_uint32; + OutHeaderView = HeaderView; + return true; + } + } + + return false; +} + +static bool +TryReadHeader(DecoderContext& Context, const CompositeBuffer& Buffer, FHeader& OutHeader, MemoryView& OutHeaderView) +{ + if (Context.HeaderOffset != MAX_uint64) + { + OutHeaderView = Buffer.ViewOrCopyRange(Context.HeaderOffset, Context.HeaderSize, Context.Header); + MakeMutableMemoryView(&OutHeader, &OutHeader + 1).CopyFrom(OutHeaderView.Left(sizeof(FHeader))); + OutHeader.ByteSwap(); + return true; + } + + if (Buffer.GetSize() < sizeof(FHeader)) + { + return false; + } + + FHeader& Header = OutHeader; + Buffer.CopyTo(MakeMutableMemoryView(&Header, &Header + 1)); + Header.ByteSwap(); + + if (const BaseDecoder* const Decoder = GetDecoder(Header.Method); Decoder && Header.Magic == FHeader::ExpectedMagic) + { + const uint64_t HeaderSize = Decoder->GetHeaderSize(Header); + const MemoryView HeaderView = Buffer.ViewOrCopyRange(0, HeaderSize, Context.Header, [](uint64_t Size) { + return UniqueBuffer::Alloc(zen::Max(NextPow2(Size), DefaultHeaderSize)); + }); + if (Header.Crc32 == FHeader::CalculateCrc32(HeaderView)) + { + Context.HeaderOffset = 0; + Context.HeaderSize = HeaderSize; + if (Context.HeaderCrc32 != Header.Crc32) + { + Context.HeaderCrc32 = Header.Crc32; + Context.RawBlockIndex = MAX_uint32; + } + OutHeaderView = HeaderView; + return true; + } + } + + return false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class ArchiveDecoderSource final : public DecoderSource +{ +public: + explicit ArchiveDecoderSource(Archive& InArchive, const uint64_t InBaseOffset) : m_Archive(InArchive), m_BaseOffset(InBaseOffset) {} + + bool Read(uint64_t Offset, MutableMemoryView Data) const final + { + m_Archive.Seek(int64_t(m_BaseOffset + Offset)); + m_Archive.Serialize(Data.GetData(), int64_t(Data.GetSize())); + return !m_Archive.IsError(); + } + + MemoryView ReadOrView(const uint64_t Offset, const uint64_t Size, DecoderContext& Context) const final + { + if (Context.CompressedBlock.GetSize() < Size) + { + Context.CompressedBlock = UniqueBuffer::Alloc(zen::Max(NextPow2(Size), DefaultBlockSize)); + } + const MutableMemoryView View = Context.CompressedBlock.GetMutableView().Left(Size); + return Read(Offset, View) ? View : MemoryView(); + } + + CompositeBuffer ReadToComposite(const uint64_t Offset, const uint64_t Size) const final + { + UniqueBuffer Buffer = UniqueBuffer::Alloc(Size); + if (Read(Offset, Buffer)) + { + return CompositeBuffer(Buffer.MoveToShared()); + } + return CompositeBuffer(); + } + +private: + Archive& m_Archive; + const uint64_t m_BaseOffset; +}; + +class BufferDecoderSource final : public DecoderSource +{ +public: + explicit BufferDecoderSource(const CompositeBuffer& InBuffer) : m_Buffer(InBuffer) {} + + bool Read(const uint64_t Offset, const MutableMemoryView Data) const final + { + if (Offset + Data.GetSize() <= m_Buffer.GetSize()) + { + m_Buffer.CopyTo(Data, Offset); + return true; + } + return false; + } + + MemoryView ReadOrView(const uint64_t Offset, const uint64_t Size, DecoderContext& Context) const final + { + return m_Buffer.ViewOrCopyRange(Offset, Size, Context.CompressedBlock, [](uint64_t BufferSize) -> UniqueBuffer { + return UniqueBuffer::Alloc(zen::Max(zen::NextPow2(BufferSize), DefaultBlockSize)); + }); + } + + CompositeBuffer ReadToComposite(const uint64_t Offset, const uint64_t Size) const final + { + return m_Buffer.Mid(Offset, Size).MakeOwned(); + } + +private: + const CompositeBuffer& m_Buffer; +}; + ////////////////////////////////////////////////////////////////////////// template<typename BufferType> @@ -1007,7 +1378,7 @@ CompressedBuffer::DecompressToComposite() const { if (const BaseDecoder* const Decoder = GetDecoder(Header.Method)) { - return Decoder->Decompress(Header, CompressedData); + return Decoder->DecompressToComposite(Header, CompressedData); } } } @@ -1041,6 +1412,240 @@ CompressedBuffer::TryGetCompressParameters(OodleCompressor& OutCompressor, return false; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +CompressedBufferReader::CompressedBufferReader(Archive& Archive) +{ + SetSource(Archive); +} + +CompressedBufferReader::CompressedBufferReader(const CompressedBuffer& Buffer) +{ + SetSource(Buffer); +} + +void +CompressedBufferReader::ResetBuffers() +{ + using namespace detail; + if (SourceArchive && Context.HeaderOffset != MAX_uint64) + { + SourceArchive->Seek(int64_t(Context.HeaderOffset)); + } + Context = DecoderContext(); +} + +void +CompressedBufferReader::ResetSource() +{ + Context.HeaderOffset = MAX_uint64; + SourceArchive = nullptr; + SourceBuffer = nullptr; +} + +void +CompressedBufferReader::SetSource(Archive& Archive) +{ + if (SourceArchive == &Archive) + { + return; + } + Context.HeaderOffset = MAX_uint64; + SourceArchive = &Archive; + SourceBuffer = nullptr; +} + +void +CompressedBufferReader::SetSource(const CompressedBuffer& Buffer) +{ + if (SourceBuffer == &Buffer) + { + return; + } + Context.HeaderOffset = MAX_uint64; + SourceArchive = nullptr; + SourceBuffer = &Buffer; +} + +uint64_t +CompressedBufferReader::GetCompressedSize() +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + return Header.TotalCompressedSize; + } + return 0; +} + +uint64_t +CompressedBufferReader::GetRawSize() +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + return Header.TotalRawSize; + } + return 0; +} + +IoHash +CompressedBufferReader::GetRawHash() +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + return IoHash::FromBLAKE3(Header.RawHash); + } + return {}; +} + +bool +CompressedBufferReader::TryGetCompressParameters(OodleCompressor& OutCompressor, + OodleCompressionLevel& OutCompressionLevel, + uint64_t& OutBlockSize) +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + return Header.TryGetCompressParameters(OutCompressor, OutCompressionLevel, OutBlockSize); + } + return false; +} + +bool +CompressedBufferReader::TryDecompressTo(const MutableMemoryView RawView, const uint64_t RawOffset) +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + const uint64_t TotalRawSize = Header.TotalRawSize; + if (RawOffset <= TotalRawSize && RawView.GetSize() <= TotalRawSize - RawOffset) + { + if (const BaseDecoder* const Decoder = GetDecoder(Header.Method)) + { + if (Decoder->TryDecompressTo( + Context, + SourceArchive ? static_cast<const DecoderSource&>(ArchiveDecoderSource(*SourceArchive, Context.HeaderOffset)) + : static_cast<const DecoderSource&>(BufferDecoderSource(SourceBuffer->GetCompressed())), + Header, + HeaderView, + RawOffset, + RawView)) + { + return true; + } + } + } + } + return false; +} + +SharedBuffer +CompressedBufferReader::Decompress(const uint64_t RawOffset, const uint64_t RawSize) +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + const uint64_t TotalRawSize = Header.TotalRawSize; + const uint64_t RawSizeToCopy = RawSize == MAX_uint64 ? TotalRawSize - RawOffset : RawSize; + if (RawOffset <= TotalRawSize && RawSizeToCopy <= TotalRawSize - RawOffset) + { + if (const BaseDecoder* const Decoder = GetDecoder(Header.Method)) + { + UniqueBuffer RawData = UniqueBuffer::Alloc(RawSizeToCopy); + if (Decoder->TryDecompressTo( + Context, + SourceArchive ? static_cast<const DecoderSource&>(ArchiveDecoderSource(*SourceArchive, Context.HeaderOffset)) + : static_cast<const DecoderSource&>(BufferDecoderSource(SourceBuffer->GetCompressed())), + Header, + HeaderView, + RawOffset, + RawData)) + { + return RawData.MoveToShared(); + } + } + } + } + return {}; +} + +CompositeBuffer +CompressedBufferReader::DecompressToComposite(const uint64_t RawOffset, const uint64_t RawSize) +{ + using namespace detail; + BufferHeader Header; + MemoryView HeaderView; + if (TryReadHeader(Header, HeaderView)) + { + const uint64_t TotalRawSize = Header.TotalRawSize; + const uint64_t RawSizeToCopy = RawSize == MAX_uint64 ? TotalRawSize - RawOffset : RawSize; + if (RawOffset <= TotalRawSize && RawSizeToCopy <= TotalRawSize - RawOffset) + { + if (const BaseDecoder* const Decoder = GetDecoder(Header.Method)) + { + return Decoder->DecompressToComposite( + Context, + SourceArchive ? static_cast<const DecoderSource&>(ArchiveDecoderSource(*SourceArchive, Context.HeaderOffset)) + : static_cast<const DecoderSource&>(BufferDecoderSource(SourceBuffer->GetCompressed())), + Header, + HeaderView, + RawOffset, + RawSizeToCopy); + } + } + } + return CompositeBuffer(); +} + +bool +CompressedBufferReader::TryReadHeader(detail::BufferHeader& OutHeader, MemoryView& OutHeaderView) +{ + using namespace detail; + if (Archive* const Archive = SourceArchive) + { + return detail::TryReadHeader(Context, *Archive, OutHeader, OutHeaderView); + } + if (const CompressedBuffer* const Buffer = SourceBuffer) + { + return detail::TryReadHeader(Context, Buffer->GetCompressed(), OutHeader, OutHeaderView); + } + return false; +} + +/** A type that sets the source of a reader upon construction and resets it upon destruction. */ +class CompressedBufferReaderSourceScope +{ +public: + inline CompressedBufferReaderSourceScope(CompressedBufferReader& InReader, Archive& InArchive) : Reader(InReader) + { + Reader.SetSource(InArchive); + } + + inline CompressedBufferReaderSourceScope(CompressedBufferReader& InReader, const CompressedBuffer& InBuffer) : Reader(InReader) + { + Reader.SetSource(InBuffer); + } + + inline ~CompressedBufferReaderSourceScope() { Reader.ResetSource(); } + +private: + CompressedBufferReader& Reader; +}; + /** ______________________ _____________________________ \__ ___/\_ _____// _____/\__ ___/ _____/ @@ -1341,6 +1946,167 @@ TEST_CASE("CompressedBuffer") } } +TEST_CASE("CompressedBufferReader") +{ + const auto GenerateData = [](size_t N) -> std::vector<uint64_t> { + std::vector<uint64_t> Data; + Data.resize(N); + for (size_t Idx = 0; Idx < Data.size(); ++Idx) + { + Data[Idx] = uint64_t(Idx); + } + return Data; + }; + + CompressedBufferReader Reader; + + SUBCASE("Decompress with offset and size") + { + const auto UncompressAndValidate = [&](const CompressedBuffer& Compressed, + const int32_t OffsetCount, + const int32_t Count, + const std::vector<uint64_t>& ExpectedValues) { + Reader.SetSource(Compressed); + { + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount, Count)))); + } + { + UniqueBuffer Uncompressed = UniqueBuffer::Alloc(Count * sizeof(uint64_t)); + if (Reader.TryDecompressTo(Uncompressed, OffsetCount * sizeof(uint64_t))) + { + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount, Count)))); + } + } + }; + + constexpr uint64_t BlockSize = 64 * sizeof(uint64_t); + constexpr int32_t N = 5000; + const std::vector<uint64_t> ExpectedValues = GenerateData(N); + + const CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)), + OodleCompressor::Mermaid, + OodleCompressionLevel::Fast, + BlockSize); + + UncompressAndValidate(Compressed, 0, N, ExpectedValues); + UncompressAndValidate(Compressed, 1, N - 1, ExpectedValues); + UncompressAndValidate(Compressed, N - 1, 1, ExpectedValues); + UncompressAndValidate(Compressed, 0, 1, ExpectedValues); + UncompressAndValidate(Compressed, 2, 4, ExpectedValues); + UncompressAndValidate(Compressed, 0, 512, ExpectedValues); + UncompressAndValidate(Compressed, 3, 514, ExpectedValues); + UncompressAndValidate(Compressed, 256, 512, ExpectedValues); + UncompressAndValidate(Compressed, 512, 512, ExpectedValues); + UncompressAndValidate(Compressed, 512, 512, ExpectedValues); + UncompressAndValidate(Compressed, 4993, 4, ExpectedValues); + } + + SUBCASE("Decompress with offset only") + { + constexpr uint64_t BlockSize = 64 * sizeof(uint64_t); + constexpr int32_t N = 1000; + const std::vector<uint64_t> ExpectedValues = GenerateData(N); + + const CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)), + OodleCompressor::Mermaid, + OodleCompressionLevel::Fast, + BlockSize); + + constexpr uint64_t OffsetCount = 150; + { + SharedBuffer Buffer = Compressed.GetCompressed().ToShared(); + BufferReader Ar(Buffer.GetData(), int64_t(Buffer.GetSize())); + CompressedBufferReaderSourceScope Source(Reader, Ar); + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount)))); + } + { + CompressedBufferReaderSourceScope Source(Reader, Compressed); + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount)))); + } + + // Short (malformed) Buffer + { + IoHash RawHash; + uint64_t RawSize = 0; + const CompressedBuffer CompressedShort = + CompressedBuffer::FromCompressed(Compressed.GetCompressed().Mid(0, Compressed.GetCompressedSize() - 128), + /* out */ RawHash, + /* out */ RawSize); + Reader.SetSource(CompressedShort); + const SharedBuffer Uncompressed = Reader.Decompress(); + CHECK(Uncompressed.IsNull()); + } + } + + SUBCASE("Decompress only block") + { + constexpr uint64_t BlockSize = 256 * sizeof(uint64_t); + constexpr int32_t N = 100; + const std::vector<uint64_t> ExpectedValues = GenerateData(N); + + const CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)), + OodleCompressor::Mermaid, + OodleCompressionLevel::Fast, + BlockSize); + + constexpr uint64_t OffsetCount = 2; + constexpr uint64_t Count = 50; + { + SharedBuffer Buffer = Compressed.GetCompressed().ToShared(); + BufferReader Ar(Buffer.GetData(), int64_t(Buffer.GetSize())); + CompressedBufferReaderSourceScope Source(Reader, Ar); + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount, Count)))); + } + { + CompressedBufferReaderSourceScope Source(Reader, Compressed); + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount, Count)))); + } + } + + SUBCASE("Decompress from an uncompressed buffer.") + { + constexpr int32_t N = 4242; + const std::vector<uint64_t> ExpectedValues = GenerateData(N); + + const CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)), + OodleCompressor::NotSet, + OodleCompressionLevel::None); + Reader.SetSource(Compressed); + + { + constexpr uint64_t OffsetCount = 0; + constexpr uint64_t Count = N; + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount, Count)))); + } + + { + constexpr uint64_t OffsetCount = 21; + constexpr uint64_t Count = 999; + const SharedBuffer Uncompressed = Reader.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)); + CHECK(Uncompressed.GetView().EqualBytes(MakeMemoryView(std::span{ExpectedValues}.subspan(OffsetCount, Count)))); + } + + // Short (malformed) Buffer + { + IoHash RawHash; + uint64_t RawSize = 0; + const CompressedBuffer CompressedShort = + CompressedBuffer::FromCompressed(Compressed.GetCompressed().Mid(0, Compressed.GetCompressedSize() - 128), + /* out */ RawHash, + /* out */ RawSize); + Reader.SetSource(CompressedShort); + const SharedBuffer Uncompressed = Reader.Decompress(); + CHECK(Uncompressed.IsNull()); + } + } +} + void compress_forcelink() { diff --git a/src/zencore/include/zencore/compositebuffer.h b/src/zencore/include/zencore/compositebuffer.h index 4e4b4d002..cc03dd156 100644 --- a/src/zencore/include/zencore/compositebuffer.h +++ b/src/zencore/include/zencore/compositebuffer.h @@ -62,8 +62,12 @@ public: [[nodiscard]] ZENCORE_API CompositeBuffer MakeOwned() &&; /** Returns the concatenation of the segments into a contiguous buffer. */ - [[nodiscard]] ZENCORE_API SharedBuffer Flatten() const&; - [[nodiscard]] ZENCORE_API SharedBuffer Flatten() &&; + [[nodiscard]] inline SharedBuffer Flatten() const& { return ToShared(); } + [[nodiscard]] inline SharedBuffer Flatten() && { return ToShared(); } + + /** Returns the concatenation of the segments into a contiguous buffer. */ + [[nodiscard]] ZENCORE_API SharedBuffer ToShared() const&; + [[nodiscard]] ZENCORE_API SharedBuffer ToShared() &&; /** Returns the middle part of the buffer by taking the size starting at the offset. */ [[nodiscard]] ZENCORE_API CompositeBuffer Mid(uint64_t Offset, uint64_t Size = ~uint64_t(0)) const; @@ -76,8 +80,12 @@ public: * @param Offset The byte offset in this buffer that the range starts at. * @param Size The number of bytes in the range to view or copy. * @param CopyBuffer The buffer to write the copy into if a copy is required. + * @param Allocator The optional allocator to use when the copy buffer is required. */ - [[nodiscard]] ZENCORE_API MemoryView ViewOrCopyRange(uint64_t Offset, uint64_t Size, UniqueBuffer& CopyBuffer) const; + [[nodiscard]] ZENCORE_API MemoryView ViewOrCopyRange(uint64_t Offset, + uint64_t Size, + UniqueBuffer& CopyBuffer, + std::function<UniqueBuffer(uint64_t Size)> Allocator = UniqueBuffer::Alloc) const; /** * Copies a range of the buffer to a contiguous region of memory. diff --git a/src/zencore/include/zencore/compress.h b/src/zencore/include/zencore/compress.h index 99ce20d8a..44431f299 100644 --- a/src/zencore/include/zencore/compress.h +++ b/src/zencore/include/zencore/compress.h @@ -6,9 +6,18 @@ #include "zencore/blake3.h" #include "zencore/compositebuffer.h" +#include "zencore/intmath.h" + +#include <limits> namespace zen { +class Archive; + +namespace detail { + struct BufferHeader; +} + enum class OodleCompressor : uint8_t { NotSet = 0, @@ -160,6 +169,115 @@ private: CompositeBuffer CompressedData; }; +namespace detail { + /** A reusable context for the compressed buffer decoder. */ + struct DecoderContext + { + /** The offset in the source at which the compressed buffer begins, otherwise MAX_uint64. */ + uint64_t HeaderOffset = MAX_uint64; + /** The size of the header if known, otherwise 0. */ + uint64_t HeaderSize = 0; + /** The CRC-32 from the header, otherwise 0. */ + uint32_t HeaderCrc32 = 0; + /** Index of the block stored in RawBlock, otherwise MAX_uint32. */ + uint32_t RawBlockIndex = MAX_uint32; + + /** A buffer used to store the header when HeaderOffset is not MAX_uint64. */ + UniqueBuffer Header; + /** A buffer used to store a raw block when a partial block read is requested. */ + UniqueBuffer RawBlock; + /** A buffer used to store a compressed block when it was not in contiguous memory. */ + UniqueBuffer CompressedBlock; + }; +} // namespace detail + +/** + * A type that stores the state needed to decompress a compressed buffer. + * + * The compressed buffer can be in memory or can be loaded from a seekable archive. + * + * The reader can be reused across multiple source buffers, which allows its temporary buffers to + * be reused if they are the right size. + * + * It is only safe to use the reader from one thread at a time. + * + * @see CompressedBuffer + */ +class CompressedBufferReader +{ +public: + /** Construct a reader with no source. */ + CompressedBufferReader() = default; + + /** Construct a reader that will read from an archive as needed. */ + explicit CompressedBufferReader(Archive& Archive); + + /** Construct a reader from an in-memory compressed buffer. */ + explicit CompressedBufferReader(const CompressedBuffer& Buffer); + + /** Release any temporary buffers that have been allocated by the reader. */ + void ResetBuffers(); + + /** Clears the reference to the source without releasing temporary buffers. */ + void ResetSource(); + + void SetSource(Archive& Archive); + void SetSource(const CompressedBuffer& Buffer); + + [[nodiscard]] inline bool HasSource() const { return SourceArchive || SourceBuffer; } + + /** Returns the size of the compressed data. Zero on error. */ + [[nodiscard]] uint64_t GetCompressedSize(); + + /** Returns the size of the raw data. Zero on error. */ + [[nodiscard]] uint64_t GetRawSize(); + + /** Returns the hash of the raw data. Zero on error. */ + [[nodiscard]] IoHash GetRawHash(); + + /** + * Returns the compressor and compression level used by this buffer. + * + * The compressor and compression level may differ from those specified when creating the buffer + * because an incompressible buffer is stored with no compression. Parameters cannot be accessed + * if this is null or uses a method other than Oodle, in which case this returns false. + * + * @return True if parameters were written, otherwise false. + */ + [[nodiscard]] bool TryGetCompressParameters(OodleCompressor& OutCompressor, + OodleCompressionLevel& OutCompressionLevel, + uint64_t& OutBlockSize); + + /** + * Decompress into a memory view that is less than or equal to the available raw size. + * + * @param RawView The view to write to. The size to read is equal to the view size. + * @param RawOffset The offset into the raw data from which to decompress. + * @return True if the requested range was decompressed, otherwise false. + */ + [[nodiscard]] bool TryDecompressTo(MutableMemoryView RawView, uint64_t RawOffset = 0); + + /** + * Decompress into an owned buffer. + * + * RawOffset must be at most the raw buffer size. RawSize may be MAX_uint64 to read the whole + * buffer from RawOffset, and must otherwise fit within the bounds of the buffer. + * + * @param RawOffset The offset into the raw data from which to decompress. + * @param RawSize The size of the raw data to read from the offset. + * @return An owned buffer containing the raw data, or null on error. + */ + [[nodiscard]] SharedBuffer Decompress(uint64_t RawOffset = 0, uint64_t RawSize = MAX_uint64); + [[nodiscard]] CompositeBuffer DecompressToComposite(uint64_t RawOffset = 0, uint64_t RawSize = MAX_uint64); + +private: + bool TryReadHeader(detail::BufferHeader& OutHeader, MemoryView& OutHeaderView); + + Archive* SourceArchive = nullptr; + const CompressedBuffer* SourceBuffer = nullptr; + detail::DecoderContext Context; +}; + void compress_forcelink(); // internal } // namespace zen diff --git a/src/zencore/include/zencore/intmath.h b/src/zencore/include/zencore/intmath.h index f24caed6e..0d87e1b78 100644 --- a/src/zencore/include/zencore/intmath.h +++ b/src/zencore/include/zencore/intmath.h @@ -7,6 +7,32 @@ #include <stdint.h> ////////////////////////////////////////////////////////////////////////// +// UE Numeric constants + +#define MIN_uint8 ((uint8_t)0x00) +#define MIN_uint16 ((uint16_t)0x0000) +#define MIN_uint32 ((uint32_t)0x00000000) +#define MIN_uint64 ((uint64_t)0x0000000000000000) +#define MIN_int8 ((int8_t)-128) +#define MIN_int16 ((int16_t)-32768) +#define MIN_int32 ((int32_t)0x80000000) +#define MIN_int64 ((int64_t)0x8000000000000000) + +#define MAX_uint8 ((uint8_t)0xff) +#define MAX_uint16 ((uint16_t)0xffff) +#define MAX_uint32 ((uint32_t)0xffffffff) +#define MAX_uint64 ((uint64_t)0xffffffffffffffff) +#define MAX_int8 ((int8_t)0x7f) +#define MAX_int16 ((int16_t)0x7fff) +#define MAX_int32 ((int32_t)0x7fffffff) +#define MAX_int64 ((int64_t)0x7fffffffffffffff) + +#define MIN_flt (1.175494351e-38F) /* min positive value */ +#define MAX_flt (3.402823466e+38F) +#define MIN_dbl (2.2250738585072014e-308) /* min positive value */ +#define MAX_dbl (1.7976931348623158e+308) + +////////////////////////////////////////////////////////////////////////// #if ZEN_COMPILER_MSC || ZEN_PLATFORM_WINDOWS # pragma intrinsic(_BitScanReverse) diff --git a/src/zencore/include/zencore/stream.h b/src/zencore/include/zencore/stream.h index 9e4996249..a9d35ef1b 100644 --- a/src/zencore/include/zencore/stream.h +++ b/src/zencore/include/zencore/stream.h @@ -79,12 +79,37 @@ public: inline uint64_t CurrentOffset() const { return m_Offset; } inline void Skip(size_t ByteCount) { m_Offset += ByteCount; }; -private: +protected: const uint8_t* m_BufferBase; uint64_t m_BufferSize; uint64_t m_Offset = 0; }; +/** + * Archive base class implementation + * + * Roughly mimics UE's FArchive class, but without all the non-essential functionality + */ +class Archive +{ +public: + /** Attempts to set the current offset into backing data storage, this will do nothing if there is no storage. */ + virtual void Seek(uint64_t InPos) = 0; + virtual int64_t Tell() = 0; + virtual void Serialize(void* V, int64_t Length) = 0; + inline bool IsError() { return false; } +}; + +class BufferReader : public Archive, private BinaryReader +{ +public: + BufferReader(const void* Buffer, uint64_t Size) : BinaryReader(Buffer, Size) {} + + virtual void Seek(uint64_t InPos) override; + virtual int64_t Tell() override; + virtual void Serialize(void* V, int64_t Length) override; +}; + void stream_forcelink(); // internal } // namespace zen diff --git a/src/zencore/include/zencore/zencore.h b/src/zencore/include/zencore/zencore.h index 02e9dd5d6..47ff2ebc2 100644 --- a/src/zencore/include/zencore/zencore.h +++ b/src/zencore/include/zencore/zencore.h @@ -68,6 +68,10 @@ # pragma warning(disable : 4324) // warning C4324: '<type>': structure was padded due to alignment specifier # pragma warning(default : 4668) // warning C4668: 'symbol' is not defined as a preprocessor macro, replacing with '0' for 'directives' # pragma warning(default : 4100) // warning C4100: 'identifier' : unreferenced formal parameter +# pragma warning( \ + disable : 4373) // '%$S': virtual function overrides '%$pS', previous versions of the compiler did not override when parameters + // only differed by const/volatile qualifiers + // https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4373 #endif #ifndef ZEN_THIRD_PARTY_INCLUDES_START diff --git a/src/zencore/stream.cpp b/src/zencore/stream.cpp index 3402e51be..ee97a53c4 100644 --- a/src/zencore/stream.cpp +++ b/src/zencore/stream.cpp @@ -51,6 +51,27 @@ BinaryWriter::Reset() } ////////////////////////////////////////////////////////////////////////// + +void +BufferReader::Seek(uint64_t InPos) +{ + // Validate range + m_Offset = InPos; +} + +int64_t +BufferReader::Tell() +{ + return CurrentOffset(); +} + +void +BufferReader::Serialize(void* V, int64_t Length) +{ + Read(V, Length); +} + +////////////////////////////////////////////////////////////////////////// // // Testing related code follows... // |