Files
UnrealEngine/Engine/Plugins/Mutable/Source/MutableRuntime/Private/MuR/ImageDataStorage.cpp
2025-05-18 13:04:45 +08:00

650 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MuR/ImageDataStorage.h"
#include "MuR/SerialisationPrivate.h"
namespace mu::FImageDataStorageInternal
{
FORCEINLINE FImageSize ComputeLODSize(FImageSize BaseSize, int32 LOD)
{
for (int32 L = 0; L < LOD; ++L)
{
BaseSize = FImageSize(
FMath::DivideAndRoundUp<uint16>(BaseSize.X, 2),
FMath::DivideAndRoundUp<uint16>(BaseSize.Y, 2));
}
return BaseSize;
}
FORCEINLINE void InitViewToBlack(const TArrayView<uint8>& View, EImageFormat Format)
{
const FImageFormatData FormatData = GetImageFormatData(Format);
constexpr uint8 ZeroBuffer[FImageFormatData::MAX_BYTES_PER_BLOCK] = {0};
const bool bIsFormatBlackBlockZeroed =
FMemory::Memcmp(ZeroBuffer, FormatData.BlackBlock, FImageFormatData::MAX_BYTES_PER_BLOCK) == 0;
if (bIsFormatBlackBlockZeroed)
{
FMemory::Memzero(View.GetData(), View.Num());
}
else
{
const int32 FormatBlockSize = FormatData.BytesPerBlock;
if (FormatData.BytesPerBlock == 0)
{
return;
}
check(FormatData.BytesPerBlock != 0);
check(FormatBlockSize <= FImageFormatData::MAX_BYTES_PER_BLOCK);
check(View.Num() % FormatBlockSize == 0);
uint8* const BufferBegin = View.GetData();
const int32 DataSize = View.Num();
for (int32 BlockDataOffset = 0; BlockDataOffset < DataSize; BlockDataOffset += FormatBlockSize)
{
FMemory::Memcpy(BufferBegin + BlockDataOffset, FormatData.BlackBlock, FormatBlockSize);
}
}
}
}
namespace mu::MemoryCounters
{
std::atomic<SSIZE_T>& FImageMemoryCounter::Get()
{
static std::atomic<SSIZE_T> Counter{0};
return Counter;
}
}
namespace mu
{
FImageDataStorage::FImageDataStorage()
{
}
FImageDataStorage::FImageDataStorage(const FImageDesc& Desc)
{
Init(Desc, EInitializationType::NotInitialized);
}
FImageDataStorage::FImageDataStorage(const FImageDataStorage& Other)
{
CopyFrom(Other);
}
FImageDataStorage& FImageDataStorage::operator=(const FImageDataStorage& Other)
{
CopyFrom(Other);
return *this;
}
bool FImageDataStorage::operator==(const FImageDataStorage& Other) const
{
const bool bSameMetadata =
ImageSize == Other.ImageSize &&
ImageFormat == Other.ImageFormat &&
NumLODs == Other.NumLODs;
if (!bSameMetadata)
{
return false;
}
if (Buffers.Num() != Other.Buffers.Num())
{
return false;
}
// Buffers are sorted from large to small, but mips usually have information about the whole image.
// Process the small ones first.
for (int32 BufferIndex = Buffers.Num() - 1; BufferIndex >= 0; --BufferIndex)
{
if (Buffers[BufferIndex] != Other.Buffers[BufferIndex])
{
return false;
}
}
return true;
}
void FImageDataStorage::InitInternalArray(int32 Index, int32 Size, EInitializationType InitType)
{
using namespace FImageDataStorageInternal;
check(Index < Buffers.Num());
Buffers[Index].SetNumUninitialized(Size);
if (InitType == EInitializationType::Black)
{
const TArrayView<uint8> BufferView = MakeArrayView(Buffers[Index].GetData(), Buffers[Index].Num());
InitViewToBlack(BufferView, ImageFormat);
}
check(InitType == EInitializationType::Black || InitType == EInitializationType::NotInitialized);
}
void FImageDataStorage::Init(const FImageDesc& ImageDesc, EInitializationType InitType)
{
using namespace FImageDataStorageInternal;
if (ImageFormat != ImageDesc.m_format || ImageSize != ImageDesc.m_size)
{
NumLODs = 0;
Buffers.Empty();
}
ImageSize = ImageDesc.m_size;
ImageFormat = ImageDesc.m_format;
SetNumLODs(ImageDesc.m_lods, EInitializationType::NotInitialized);
if (InitType == EInitializationType::Black)
{
for (FImageArray& Buffer : Buffers)
{
InitViewToBlack(MakeArrayView(Buffer.GetData(), Buffer.Num()), ImageFormat);
}
}
}
void FImageDataStorage::CopyFrom(const FImageDataStorage& Other)
{
// Copy images that have different format is not allowed.
check(ImageFormat == EImageFormat::None || Other.ImageFormat == ImageFormat);
ImageSize = Other.ImageSize;
ImageFormat = Other.ImageFormat;
NumLODs = Other.NumLODs;
Buffers = Other.Buffers;
CompactedTailOffsets = Other.CompactedTailOffsets;
}
void FImageDataStorage::SetNumLODs(int32 InNumLODs, EInitializationType InitType)
{
using namespace FImageDataStorageInternal;
check(!IsVoid());
if (InNumLODs == NumLODs)
{
return;
}
const int32 FirstCompactedTailLOD = ComputeFirstCompactedTailLOD();
const int32 ValidNumLODsToSet = FMath::Min(InNumLODs, ComputeNumLODsForSize(ImageSize));
const int32 NumOldLODsInTail = FMath::Max(0, NumLODs - FirstCompactedTailLOD);
const int32 NumOldLODsNotInTail = FMath::Max(0, NumLODs - NumOldLODsInTail);
const int32 NumNewLODsInTail = FMath::Max(0, ValidNumLODsToSet - FirstCompactedTailLOD);
const int32 NumNewLODsNotInTail = FMath::Max(0, ValidNumLODsToSet - NumNewLODsInTail);
Buffers.SetNum(NumNewLODsNotInTail + static_cast<int32>(NumNewLODsInTail > 0));
const int32 BlockSizeX = GetImageFormatData(ImageFormat).PixelsPerBlockX;
const int32 BlockSizeY = GetImageFormatData(ImageFormat).PixelsPerBlockY;
const int32 BytesPerBlock = GetImageFormatData(ImageFormat).BytesPerBlock;
// Do not allocate memory for formats like RLE that are not block based.
if (BlockSizeX == 0 || BlockSizeY == 0 || BytesPerBlock == 0)
{
NumLODs = ValidNumLODsToSet;
return;
}
FImageSize LODDims = ComputeLODSize(ImageSize, NumOldLODsNotInTail);
// Update non tail buffers.
{
const int32 NumAddedLODsNotInTail = FMath::Max(0, NumNewLODsNotInTail - NumOldLODsNotInTail);
for (int32 L = 0; L < NumAddedLODsNotInTail; ++L)
{
const int32 BlocksX = FMath::DivideAndRoundUp<int32>(LODDims.X, BlockSizeX);
const int32 BlocksY = FMath::DivideAndRoundUp<int32>(LODDims.Y, BlockSizeY);
const int32 DataSize = BlocksX * BlocksY * BytesPerBlock;
InitInternalArray(L + NumOldLODsNotInTail, DataSize, InitType);
LODDims = FImageSize(
FMath::DivideAndRoundUp<uint16>(LODDims.X, 2),
FMath::DivideAndRoundUp<uint16>(LODDims.Y, 2));
}
}
// Update tail buffer.
if (NumNewLODsInTail > 0)
{
const int32 NumAddedLODsInTail = FMath::Max(0, NumNewLODsInTail - NumOldLODsInTail);
int32 TailBufferSize = 0;
for (int32 L = 0; L < NumNewLODsInTail; ++L)
{
const int32 BlocksX = FMath::DivideAndRoundUp<int32>(LODDims.X, BlockSizeX);
const int32 BlocksY = FMath::DivideAndRoundUp<int32>(LODDims.Y, BlockSizeY);
TailBufferSize += BlocksX * BlocksY * BytesPerBlock;
CompactedTailOffsets[L] = TailBufferSize;
LODDims = FImageSize(
FMath::DivideAndRoundUp<uint16>(LODDims.X, 2),
FMath::DivideAndRoundUp<uint16>(LODDims.Y, 2));
}
FImageArray& TailBuffer = Buffers.Last();
TailBuffer.SetNumUninitialized(TailBufferSize);
if (NumAddedLODsInTail > 0 && InitType == EInitializationType::Black)
{
const int32 FirstAddedLODInTailOffset = NumOldLODsInTail == 0 ? 0 : CompactedTailOffsets[NumOldLODsInTail - 1];
InitViewToBlack(MakeArrayView(TailBuffer.GetData() + FirstAddedLODInTailOffset, TailBuffer.Num()), ImageFormat);
}
// Fix the unused tail offsets, this is important to make sure serialized data is deterministic.
const int32 UnusedTailOffsetValue = CompactedTailOffsets[NumNewLODsInTail - 1];
for (int32 I = NumNewLODsInTail; I < NumLODsInCompactedTail; ++I)
{
CompactedTailOffsets[I] = UnusedTailOffsetValue;
}
}
NumLODs = ValidNumLODsToSet;
}
void FImageDataStorage::DropLODs(int32 NumLODsToDrop)
{
check(!IsVoid());
if (NumLODsToDrop >= NumLODs)
{
//check(false);
Buffers.Empty();
ImageSize = FImageSize(0, 0);
NumLODs = 0;
return;
}
const int32 FirstCompactedTailLOD = ComputeFirstCompactedTailLOD();
const int32 NumLODsToDropInTail = FMath::Max(0, NumLODsToDrop - FirstCompactedTailLOD);
for (int32 DestBufferIndex = 0, BufferIndex = NumLODsToDrop - NumLODsToDropInTail;
BufferIndex < FirstCompactedTailLOD;
++DestBufferIndex, ++BufferIndex)
{
Buffers[DestBufferIndex] = MoveTemp(Buffers[BufferIndex]);
}
if (NumLODsToDropInTail)
{
int32 TailBufferOffset = NumLODsToDropInTail == 0 ? 0 : CompactedTailOffsets[NumLODsToDropInTail - 1];
FImageArray& TailBuffer = Buffers.Last();
int32 NewBufferSize = TailBuffer.Num() - TailBufferOffset;
FMemory::Memmove(TailBuffer.GetData(), TailBuffer.GetData() + TailBufferOffset, NewBufferSize);
TailBuffer.SetNum(NewBufferSize);
int32 NumLODsInTail = FMath::Max(0, NumLODs - FirstCompactedTailLOD);
int32 OffsetIndex = NumLODsToDropInTail;
for (int32 DestOffsetsIndex = 0; OffsetIndex < NumLODsInTail; ++DestOffsetsIndex, ++OffsetIndex)
{
CompactedTailOffsets[DestOffsetsIndex] = CompactedTailOffsets[OffsetIndex];
}
// Fix the unused tail offsets, this is important to make sure serialized data is deterministic.
const uint32 UnusedTailValue = NumLODsInTail - 1 <= 0 ? 0 : CompactedTailOffsets[NumLODsInTail - 1];
for (; OffsetIndex < NumLODsInCompactedTail; ++OffsetIndex)
{
CompactedTailOffsets[OffsetIndex] = UnusedTailValue;
}
}
// Update metadata.
NumLODs = FMath::Max(0, NumLODs - NumLODsToDrop);
for (int32 I = 0; I < NumLODsToDrop; ++I)
{
ImageSize = FImageSize(
FMath::DivideAndRoundUp<uint16>(ImageSize.X, 2),
FMath::DivideAndRoundUp<uint16>(ImageSize.Y, 2));
}
}
void FImageDataStorage::ResizeLOD(int32 LODIndex, int32 NewSizeBytes)
{
check(!IsVoid());
check(NewSizeBytes >= 0);
const int32 FirstCompactedTailLOD = ComputeFirstCompactedTailLOD();
FImageArray& Buffer = GetInternalArray(LODIndex);
if (LODIndex < FirstCompactedTailLOD)
{
Buffer.SetNum(NewSizeBytes);
return;
}
check(&Buffer == &Buffers.Last());
// Compacted tail resize.
const int32 LODSize = GetLOD(LODIndex).Num();
const int32 OldBufferSize = Buffer.Num();
const int32 SizeDifference = NewSizeBytes - LODSize;
const int32 NewBufferSize = OldBufferSize + SizeDifference;
// If the buffer is growing we need to resize before moving the content.
if (SizeDifference > 0)
{
Buffer.SetNum(NewBufferSize);
}
// Move content and update tail offsets.
const int32 LODIndexInTail = LODIndex - FirstCompactedTailLOD;
// Last LOD does not need to move the content.
if (LODIndex < NumLODs - 1)
{
const int32 LODEndOffset = CompactedTailOffsets[LODIndexInTail];
FMemory::Memmove(
Buffer.GetData() + LODEndOffset + SizeDifference,
Buffer.GetData() + LODEndOffset,
OldBufferSize - LODEndOffset);
}
// If the buffer is shrinking we need to resize after moving the content.
if (SizeDifference < 0)
{
Buffer.SetNum(NewBufferSize);
}
// Tail buffer offsets are represented with uint16 for compactness, check any overflow.
check(Buffer.Num() < TNumericLimits<uint16>::Max());
// Keep offset updated even if there is no LOD so that serialization can be deterministic.
for (int32 I = LODIndexInTail; I < NumLODsInCompactedTail; ++I)
{
check(int32(CompactedTailOffsets[I]) + SizeDifference >= 0);
CompactedTailOffsets[I] += SizeDifference;
}
}
TArrayView<const uint8> FImageDataStorage::GetLOD(int32 LODIndex) const
{
check(!IsVoid());
if (LODIndex >= NumLODs)
{
//check(false);
return TArrayView<const uint8>();
}
const int32 FirstTailLOD = ComputeFirstCompactedTailLOD();
if (LODIndex >= FirstTailLOD)
{
const int32 LODIndexInTail = FMath::Max(0, LODIndex - FirstTailLOD);
const int32 LODInTailBegin = LODIndexInTail == 0 ? 0 : CompactedTailOffsets[LODIndexInTail - 1];
const int32 LODInTailEnd = CompactedTailOffsets[LODIndexInTail];
const FImageArray& TailBuffer = Buffers.Last();
return MakeArrayView(TailBuffer.GetData() + LODInTailBegin, LODInTailEnd - LODInTailBegin);
}
return MakeArrayView(Buffers[LODIndex].GetData(), Buffers[LODIndex].Num());
}
TArrayView<uint8> FImageDataStorage::GetLOD(int32 LODIndex)
{
const TArrayView<const uint8> ConstLODView = const_cast<const FImageDataStorage*>(this)->GetLOD(LODIndex);
return TArrayView<uint8>(const_cast<uint8*>(ConstLODView.GetData()), ConstLODView.Num());
}
int32 FImageDataStorage::GetNumBatchesLODRange(int32 BatchSizeInElems, int32 BatchElemSizeInBytes, int32 LODBegin, int32 LODEnd) const
{
check(!IsVoid());
check(LODBegin < LODEnd);
check(LODBegin >= 0 && LODEnd <= NumLODs);
const int32 BatchNumBytes = BatchSizeInElems*BatchElemSizeInBytes;
const int32 FirstCompactedTailLOD = ComputeFirstCompactedTailLOD();
const int32 NumLODsInTail = FMath::Max(0, NumLODs - FirstCompactedTailLOD);
const int32 NumLODsNotInTail = FMath::Max(0, NumLODs - NumLODsInTail);
const int32 LODsNotInTailLODRangeEnd = FMath::Min(LODEnd, NumLODsNotInTail);
int32 NumBatches = 0;
for (int32 BufferIndex = LODBegin; BufferIndex < LODsNotInTailLODRangeEnd; ++BufferIndex)
{
NumBatches += FMath::DivideAndRoundUp(Buffers[BufferIndex].Num(), BatchNumBytes);
}
if (FirstCompactedTailLOD < LODEnd)
{
TArrayView<const uint8> FirstLODInRangeView = GetLOD(FMath::Max(FirstCompactedTailLOD, LODBegin));
TArrayView<const uint8> LastLODInRangeView = GetLOD(LODEnd - 1);
const int32 TailInLODRangeNumBytes = LastLODInRangeView.GetData() - FirstLODInRangeView.GetData() + LastLODInRangeView.Num();
NumBatches += FMath::DivideAndRoundUp(TailInLODRangeNumBytes, BatchNumBytes);
}
return NumBatches;
}
int32 FImageDataStorage::GetNumBatches(int32 BatchSizeInElems, int32 BatchElemSizeInBytes) const
{
return GetNumBatchesLODRange(BatchSizeInElems, BatchElemSizeInBytes, 0, NumLODs);
}
TArrayView<const uint8> FImageDataStorage::GetBatch(int32 BatchId, int32 BatchSizeInElems, int32 BatchElemSizeInBytes) const
{
return GetBatchLODRange(BatchId, BatchSizeInElems, BatchElemSizeInBytes, 0, NumLODs);
}
TArrayView<uint8> FImageDataStorage::GetBatch(int32 BatchId, int32 BatchSizeInElems, int32 BatchElemSizeInBytes)
{
// Use the const_cast idiom to generate the non-const operation.
const TArrayView<const uint8> ConstBatchView =
const_cast<const FImageDataStorage*>(this)->GetBatch(BatchId, BatchSizeInElems, BatchElemSizeInBytes);
return TArrayView<uint8>(const_cast<uint8*>(ConstBatchView.GetData()), ConstBatchView.Num());
}
int32 FImageDataStorage::GetNumBatchesFirstLODOffset(int32 BatchSizeInElems, int32 BatchElemSizeInBytes, int32 OffsetInBytes) const
{
check(!IsVoid());
TArrayView<const uint8> FirstLODView = GetLOD(0);
check(OffsetInBytes % BatchElemSizeInBytes == 0);
check(FirstLODView.Num() % BatchElemSizeInBytes == 0);
check(FirstLODView.Num() > OffsetInBytes);
return FMath::DivideAndRoundUp(FirstLODView.Num() - OffsetInBytes, BatchSizeInElems*BatchElemSizeInBytes);
}
TArrayView<const uint8> FImageDataStorage::GetBatchFirstLODOffset(int32 BatchId, int32 BatchSizeInElems, int32 BatchElemSizeInBytes, int32 OffsetInBytes) const
{
check(!IsVoid());
TArrayView<const uint8> FirstLODView = GetLOD(0);
check(OffsetInBytes < FirstLODView.Num());
check(FirstLODView.Num() % BatchElemSizeInBytes == 0);
check(OffsetInBytes % BatchElemSizeInBytes == 0);
TArrayView<const uint8> OffsetedLODView = MakeArrayView(FirstLODView.GetData() + OffsetInBytes, FirstLODView.Num() - OffsetInBytes);
const int32 BatchSizeInBytes = BatchSizeInElems * BatchElemSizeInBytes;
const int32 BatchOffset = BatchId * BatchSizeInBytes;
if (BatchOffset >= OffsetedLODView.Num())
{
return TArrayView<const uint8>();
}
return MakeArrayView(OffsetedLODView.GetData() + BatchOffset, FMath::Min(BatchSizeInBytes, OffsetedLODView.Num() - BatchOffset));
}
TArrayView<uint8> FImageDataStorage::GetBatchFirstLODOffset(int32 BatchId, int32 BatchSizeInElems, int32 BatchElemSizeInBytes, int32 OffsetInBytes)
{
const TArrayView<const uint8> ConstBatchView =
const_cast<const FImageDataStorage*>(this)->GetBatchFirstLODOffset(BatchId, BatchSizeInElems, BatchElemSizeInBytes, OffsetInBytes);
return TArrayView<uint8>(const_cast<uint8*>(ConstBatchView.GetData()), ConstBatchView.Num());
}
TArrayView<const uint8> FImageDataStorage::GetBatchLODRange(int32 BatchId, int32 BatchSizeInElems, int32 BatchElemSizeInBytes, int32 LODBegin, int32 LODEnd) const
{
check(!IsVoid());
check(LODBegin < LODEnd);
check(LODBegin >= 0 && LODEnd <= NumLODs);
const int32 BatchNumBytes = BatchSizeInElems*BatchElemSizeInBytes;
const int32 FirstCompactedTailLOD = ComputeFirstCompactedTailLOD();
const int32 NonTailBuffersEnd = FMath::Min(LODEnd, FirstCompactedTailLOD);
int32 NumBatches = 0;
for (int32 BufferIndex = LODBegin; BufferIndex < NonTailBuffersEnd; ++BufferIndex)
{
const TArrayView<const uint8> BufferView = MakeArrayView(Buffers[BufferIndex].GetData(), Buffers[BufferIndex].Num());
const int32 BufferNumBatches = FMath::DivideAndRoundUp(BufferView.Num(), BatchNumBytes);
check(BatchId >= NumBatches);
int32 BufferBatchId = BatchId - NumBatches;
if (BufferBatchId < BufferNumBatches)
{
const int32 BufferBatchBeginOffset = BufferBatchId * BatchNumBytes;
return TArrayView<const uint8>(
BufferView.GetData() + BufferBatchBeginOffset,
FMath::Min(BufferView.Num() - BufferBatchBeginOffset, BatchNumBytes));
}
NumBatches += BufferNumBatches;
}
if (FirstCompactedTailLOD < LODEnd)
{
TArrayView<const uint8> FirstLODInRangeView = GetLOD(FMath::Max(FirstCompactedTailLOD, LODBegin));
TArrayView<const uint8> LastLODInRangeView = GetLOD(LODEnd - 1);
const int32 TailInLODRangeNumBytes = LastLODInRangeView.GetData() - FirstLODInRangeView.GetData() + LastLODInRangeView.Num();
TArrayView<const uint8> TailInLODRangeView = MakeArrayView(FirstLODInRangeView.GetData(), TailInLODRangeNumBytes);
check(BatchId >= NumBatches);
const int32 TailInLODRangeBatchId = BatchId - NumBatches;
const int32 TailInLODRangeNumBatches = FMath::DivideAndRoundUp(TailInLODRangeView.Num(), BatchNumBytes);
if (TailInLODRangeBatchId < TailInLODRangeNumBatches)
{
const int32 BufferBatchBeginOffset = TailInLODRangeBatchId * BatchNumBytes;
return TArrayView<const uint8>(
TailInLODRangeView.GetData() + BufferBatchBeginOffset,
FMath::Min(TailInLODRangeView.Num() - BufferBatchBeginOffset, BatchNumBytes));
}
}
// If the BatchId is not found, return an empty view.
return TArrayView<const uint8>();
}
TArrayView<uint8> FImageDataStorage::GetBatchLODRange(int32 BatchId, int32 BatchSizeInElems, int32 BatchElemSizeInBytes, int32 LODBegin, int32 LODEnd)
{
const TArrayView<const uint8> ConstBatchView =
const_cast<const FImageDataStorage*>(this)->GetBatchLODRange(
BatchId, BatchSizeInElems, BatchElemSizeInBytes, LODBegin, LODEnd);
return TArrayView<uint8>(const_cast<uint8*>(ConstBatchView.GetData()), ConstBatchView.Num());
}
int32 FImageDataStorage::GetAllocatedSize() const
{
SIZE_T Result = 0;
for (const FImageArray& Buffer : Buffers)
{
Result += Buffer.GetAllocatedSize();
}
check(Result < TNumericLimits<int32>::Max())
return (int32)Result;
}
int32 FImageDataStorage::GetDataSize() const
{
SIZE_T Result = 0;
for (const FImageArray& Buffer : Buffers)
{
Result += Buffer.Num();
}
check(Result < TNumericLimits<int32>::Max())
return (int32)Result;
}
bool FImageDataStorage::IsEmpty() const
{
check(!IsVoid());
for (const FImageArray& Buffer : Buffers)
{
if (!Buffer.IsEmpty())
{
return false;
}
}
return true;
}
void FImageDataStorage::Serialise(FOutputArchive& Arch) const
{
Arch << ImageSize.X;
Arch << ImageSize.Y;
Arch << ImageFormat;
Arch << NumLODs;
Arch << Buffers.Num();
for (const FImageArray& Buffer : Buffers)
{
Arch << Buffer;
}
Arch << CompactedTailOffsets.Num();
Arch << CompactedTailOffsets;
}
void FImageDataStorage::Unserialise(FInputArchive& Arch)
{
Arch >> ImageSize.X;
Arch >> ImageSize.Y;
Arch >> ImageFormat;
Arch >> NumLODs;
int32 BuffersNum = 0;
Arch >> BuffersNum;
Buffers.SetNum(BuffersNum);
for (int32 I = 0; I < BuffersNum; ++I)
{
Arch >> Buffers[I];
}
int32 NumTailOffsets = 0;
Arch >> NumTailOffsets;
check(NumTailOffsets == CompactedTailOffsets.Num());
Arch >> CompactedTailOffsets;
}
}