Files
UnrealEngine/Engine/Plugins/Runtime/ReplicationSystemTestPlugin/Source/Private/Tests/Serialization/TestCompressedChangeMask.cpp
2025-05-18 13:04:45 +08:00

361 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "NetworkAutomationTest.h"
#include "NetworkAutomationTestMacros.h"
#include "TestCompressedChangeMaskData.h"
#include "Net/Core/NetBitArray.h"
#include "Iris/Serialization/NetBitStreamReader.h"
#include "Iris/Serialization/NetBitStreamWriter.h"
#include "Iris/Serialization/NetBitStreamUtil.h"
namespace UE::Net::Private
{
class FCompressedChangeMaskTest : public FNetworkAutomationTestSuiteFixture
{
public:
typedef FNetBitArrayView::StorageWordType FWordType;
typedef void (*WriteSparseBitArrayFunc)(FNetBitStreamWriter* Writer, const uint32* Data, uint32 BitCount, ESparseBitArraySerializationHint Hint);
typedef void (*ReadSparseBitArrayFunc)(FNetBitStreamReader* Reader, uint32* OutData, uint32 BitCount, ESparseBitArraySerializationHint Hint);
typedef void (*WriteSparseBitArrayDeltaFunc)(FNetBitStreamWriter* Writer, const uint32* Data, const uint32* BaseData, uint32 BitCount);
typedef void (*ReadSparseBitArrayDeltaFunc)(FNetBitStreamReader* Reader, uint32* OutData, const uint32* BaseData, uint32 BitCount);
protected:
struct FChangeMaskStats
{
uint32 TotalChangeMaskBits = 0U;
uint32 TotalSetBits = 0U;
uint32 TotalWrittenBits = 0U;
uint32 Count = 0U;
};
class FChangeMaskBuffer
{
public:
FChangeMaskBuffer()
{
Reset();
}
inline FWordType* GetBuffer() { return &Buffer[0]; }
uint32 GetBufferCapacity() const { return sizeof(Buffer); }
private:
void Reset() {}
FWordType Buffer[1024];
};
template <typename T>
bool ValidateTestData(const T& TestData)
{
const uint32 TestDataWordCount = UE_ARRAY_COUNT(TestData);
bool bResult = true;
SIZE_T Offset = 0;
while (Offset + 2U < TestDataWordCount)
{
const FWordType BitCount = TestData[Offset];
const FWordType Invert = TestData[Offset + 1];
Offset += FNetBitArrayView::CalculateRequiredWordCount(BitCount) + 2U;
}
return Offset == TestDataWordCount;
}
template<typename FunctorT, typename T> void ForAllEntries(FunctorT&& Functor, const T& TestData) const
{
const uint32 TestDataWordCount = UE_ARRAY_COUNT(TestData);
bool bResult = true;
SIZE_T Offset = 0;
while (Offset + 2U < TestDataWordCount)
{
const FWordType BitCount = TestData[Offset];
const FWordType Invert = TestData[Offset + 1];
const FWordType* Data = &TestData[Offset + 2];
Functor(BitCount, Invert, Data);
Offset += FNetBitArrayView::CalculateRequiredWordCount(BitCount) + 2U;
}
}
template <typename T>
bool ValidateDeltaTestData(const T& DeltaTestData, const T& BaseTestData)
{
const uint32 TestDataWordCount = UE_ARRAY_COUNT(DeltaTestData);
if (TestDataWordCount != UE_ARRAY_COUNT(BaseTestData))
{
return false;
}
bool bResult = true;
SIZE_T Offset = 0;
while (Offset + 2U < TestDataWordCount)
{
const FWordType BitCount = DeltaTestData[Offset];
const FWordType Invert = DeltaTestData[Offset + 1];
if (BitCount != BaseTestData[Offset])
{
return false;
}
Offset += FNetBitArrayView::CalculateRequiredWordCount(BitCount) + 2U;
}
return Offset == TestDataWordCount;
}
template<typename FunctorT, typename T> void ForAllEntriesDelta(FunctorT&& Functor, const T& DeltaTestData, const T& BaseTestData) const
{
const uint32 TestDataWordCount = UE_ARRAY_COUNT(DeltaTestData);
check(TestDataWordCount == UE_ARRAY_COUNT(BaseTestData));
bool bResult = true;
SIZE_T Offset = 0;
while (Offset + 2U < TestDataWordCount)
{
const FWordType BitCount = DeltaTestData[Offset];
const FWordType Invert = BaseTestData[Offset + 1];
const FWordType* DeltaData = &DeltaTestData[Offset + 2];
const FWordType* BaseData = &BaseTestData[Offset + 2];
Functor(BitCount, Invert, DeltaData, BaseData);
Offset += FNetBitArrayView::CalculateRequiredWordCount(BitCount) + 2U;
}
}
FChangeMaskBuffer WriteBuffer;
FChangeMaskBuffer TempChangeMaskData;
FNetBitStreamWriter Writer;
FNetBitStreamReader Reader;
};
extern const FCompressedChangeMaskTest::FWordType FullTestData[];
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, TestValidateTestData)
{
const FCompressedChangeMaskTest::FWordType BadChangeMaskTestData[] =
{
/* BitCount */ 32U, /* Invert */ 0U, /* Data */ 0x1, 0x2,
/* BitCount */ 64U, /* Invert */ 0U, /* Data */ 0x1, 0x2
};
const FCompressedChangeMaskTest::FWordType GoodChangeMaskTestData[] =
{
/* BitCount */ 32U, /* Invert */ 0U, /* Data */ 0x1,
/* BitCount */ 32U, /* Invert */ 1U, /* Data */ 0xFFFFFFFF,
};
UE_NET_ASSERT_FALSE(ValidateTestData(BadChangeMaskTestData));
UE_NET_ASSERT_TRUE(ValidateTestData(GoodChangeMaskTestData));
}
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, TestBasic)
{
Writer.InitBytes(WriteBuffer.GetBuffer(), WriteBuffer.GetBufferCapacity());
const uint32 StartPos = Writer.GetPosBits();
UE_NET_ASSERT_EQ(StartPos, 0U);
const bool bIsOverflown = Writer.IsOverflown();
UE_NET_ASSERT_FALSE(bIsOverflown);
}
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, TestValidateFullTestData)
{
UE_NET_ASSERT_TRUE(ValidateTestData(FullTestData));
}
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, WriteEmptyChangeMask)
{
const FCompressedChangeMaskTest::FWordType TestData[] =
{
/* BitCount , Invert, Data,,, */
32U, 0U, 0x0,
1024U, 0U, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
};
UE_NET_ASSERT_TRUE(ValidateTestData(TestData));
auto&& TestWriteFunc = [this](FWordType BitCount, FWordType Invert, const FWordType* Data)
{
Writer.InitBytes(WriteBuffer.GetBuffer(), WriteBuffer.GetBufferCapacity());
WriteSparseBitArray(&Writer, Data, BitCount, Invert ? ESparseBitArraySerializationHint::ContainsMostlyOnes : ESparseBitArraySerializationHint::None);
// Debug data
const FNetBitArrayView ChangeMask = MakeNetBitArrayView(Data, BitCount);
const uint32 NumSetBits = ChangeMask.CountSetBits();
//UE_LOG(LogTemp, Log, TEXT("WroteSparseArray with SetBits %u / %u using %u Bits"), NumSetBits, BitCount, Writer.GetPosBits());
};
ForAllEntries(TestWriteFunc, TestData);
}
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, WriteChangeMasks)
{
FChangeMaskStats ChangeMaskStats;
auto&& TestWriteFunc = [this, &ChangeMaskStats](FWordType BitCount, FWordType Invert, const FWordType* Data)
{
Writer.InitBytes(WriteBuffer.GetBuffer(), WriteBuffer.GetBufferCapacity());
WriteSparseBitArray(&Writer, Data, BitCount, Invert ? ESparseBitArraySerializationHint::ContainsMostlyOnes : ESparseBitArraySerializationHint::None);
// Debug data
const FNetBitArrayView ChangeMask = MakeNetBitArrayView(Data, BitCount);
const uint32 NumSetBits = ChangeMask.CountSetBits();
//UE_LOG(LogTemp, Log, TEXT("WroteSparseArray with SetBits %u / %u using %u Bits"), NumSetBits, BitCount, Writer.GetPosBits());
ChangeMaskStats.TotalChangeMaskBits += BitCount;
ChangeMaskStats.TotalSetBits += NumSetBits;
ChangeMaskStats.TotalWrittenBits += Writer.GetPosBits();
++ChangeMaskStats.Count;
};
ForAllEntries(TestWriteFunc, FullTestData);
// Log stats
if (ChangeMaskStats.TotalChangeMaskBits > 0)
{
float Ratio = static_cast<float>(ChangeMaskStats.TotalWrittenBits) / static_cast<float>(ChangeMaskStats.TotalChangeMaskBits);
float WrittenBitsPerDirtyBit = static_cast<float>(ChangeMaskStats.TotalWrittenBits) / static_cast<float>(ChangeMaskStats.TotalSetBits);
UE_NET_LOG(TEXT("")) << TStringBuilder<128>().Appendf(TEXT("WriteChangeMasks::Summary SetBits %u / TotalBits %u WrittenBits %u Ratio %f BitsPerDirtyBit %f"), ChangeMaskStats.TotalSetBits, ChangeMaskStats.TotalChangeMaskBits, ChangeMaskStats.TotalWrittenBits, Ratio, WrittenBitsPerDirtyBit);
}
}
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, WriteAndReadChangeMasks)
{
WriteSparseBitArrayFunc WriteFunc = WriteSparseBitArray;
ReadSparseBitArrayFunc ReadFunc = ReadSparseBitArray;
auto&& TestWriteAndReadFunc = [this, WriteFunc, ReadFunc](FWordType BitCount, FWordType Invert, const FWordType* Data)
{
Writer.InitBytes(WriteBuffer.GetBuffer(), WriteBuffer.GetBufferCapacity());
WriteFunc(&Writer, Data, BitCount, Invert ? ESparseBitArraySerializationHint::ContainsMostlyOnes : ESparseBitArraySerializationHint::None);
Writer.CommitWrites();
UE_NET_ASSERT_FALSE(Writer.IsOverflown());
Reader.InitBits(WriteBuffer.GetBuffer(), Writer.GetPosBits());
// Init changemask
FNetBitArrayView RcvdChangeMask((FWordType*)TempChangeMaskData.GetBuffer(), BitCount, FNetBitArrayView::ResetOnInit);
// Read changemask
ReadFunc(&Reader, TempChangeMaskData.GetBuffer(), BitCount, Invert ? ESparseBitArraySerializationHint::ContainsMostlyOnes : ESparseBitArraySerializationHint::None);
UE_NET_ASSERT_FALSE(Reader.IsOverflown());
UE_NET_ASSERT_EQ(Writer.GetPosBits(), Reader.GetPosBits());
// Verify change mask
const FNetBitArrayView OriginalChangeMask = MakeNetBitArrayView(Data, BitCount);
UE_NET_ASSERT_TRUE(OriginalChangeMask == RcvdChangeMask);
};
ForAllEntries(TestWriteAndReadFunc, SingleChangeMaskTestData);
ForAllEntries(TestWriteAndReadFunc, FullTestData);
ForAllEntries(TestWriteAndReadFunc, VeryLargeChangeMaskTestData);
}
UE_NET_TEST_FIXTURE(FCompressedChangeMaskTest, WriteAndReadSparseBitArrayDelta)
{
WriteSparseBitArrayDeltaFunc WriteFunc = WriteSparseBitArrayDelta;
ReadSparseBitArrayDeltaFunc ReadFunc = ReadSparseBitArrayDelta;
// Test some basic scenarios for delta
const FCompressedChangeMaskTest::FWordType BaseTestData[] =
{
// Set one bit in each word
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x0U, 0x0U,
// Set highest bit
/* BitCount */ 32U, /* Invert */ 0U, /* Data */ 0x1U,
// Set many bit
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x0U, 0x0U,
// Clear single bit
/* BitCount */ 32U, /* Invert */ 0U, /* Data */ 0xFFFFFFFFU,
// Test inverted clear single bit (invert does not make much sense as we delta using xor
/* BitCount */ 32U, /* Invert */ 1U, /* Data */ 0xFFFFFFFFU,
// Test no change
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x0U, 0x0U,
// Test no change
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0xFFFFFFFFU, 0x1U,
// Test all changed
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x0U, 0x0U,
// Test all changed
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0xFFFFFFFFU, 0x1U,
};
const FCompressedChangeMaskTest::FWordType DeltaTestData[] =
{
// Set one bit in each word
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x1U, 0x1U,
// Set highest bit
/* BitCount */ 32U, /* Invert */ 0U, /* Data */ 0x1U | 0x8U,
// Set many bit
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0xFFFFFFFFU, 0x0U,
// Clear single bit
/* BitCount */ 32U, /* Invert */ 0U, /* Data */ 0xFFFFFFFEU,
// Test inverted clear single bit (invert does not make much sense as we delta using xor
/* BitCount */ 32U, /* Invert */ 1U, /* Data */ 0xFFFFFFFFU & 0xFFFFFFFEU,
// Test no change
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x0U, 0x0U,
// Test no change
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0xFFFFFFFFU, 0x1U,
// Test all changed
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0xFFFFFFFFU, 0x1U,
// Test all changed
/* BitCount */ 33U, /* Invert */ 0U, /* Data */ 0x0U, 0x0U,
};
UE_NET_ASSERT_TRUE(ValidateDeltaTestData(DeltaTestData, BaseTestData));
auto&& TestWriteAndReadFunc = [this, WriteFunc, ReadFunc](FWordType BitCount, FWordType Invert, const FWordType* Data, const FWordType* BaseData)
{
Writer.InitBytes(WriteBuffer.GetBuffer(), WriteBuffer.GetBufferCapacity());
WriteFunc(&Writer, Data, BaseData, BitCount);
Writer.CommitWrites();
UE_NET_ASSERT_FALSE(Writer.IsOverflown());
Reader.InitBits(WriteBuffer.GetBuffer(), Writer.GetPosBits());
// Init changemask
FNetBitArrayView RcvdChangeMask((FWordType*)TempChangeMaskData.GetBuffer(), BitCount, FNetBitArrayView::ResetOnInit);
// Read changemask
ReadFunc(&Reader, TempChangeMaskData.GetBuffer(), BaseData, BitCount);
UE_NET_ASSERT_FALSE(Reader.IsOverflown());
UE_NET_ASSERT_EQ(Writer.GetPosBits(), Reader.GetPosBits());
// Verify change mask
const FNetBitArrayView OriginalChangeMask = MakeNetBitArrayView(Data, BitCount);
UE_NET_ASSERT_TRUE(OriginalChangeMask == RcvdChangeMask);
};
ForAllEntriesDelta(TestWriteAndReadFunc, DeltaTestData, BaseTestData);
}
}