net.SupportFastArrayDelta
net.SupportFastArrayDelta
#Overview
name: net.SupportFastArrayDelta
This variable is created as a Console Variable (cvar).
- type:
Var
- help:
Whether or not Fast Array Struct Delta Serialization is enabled.
It is referenced in 3
C++ source files.
#Summary
#Usage in the C++ source code
The purpose of net.SupportFastArrayDelta is to enable or disable Fast Array Struct Delta Serialization in Unreal Engine’s networking system. This setting is used to optimize network replication for arrays of structs by only sending changed properties instead of the entire array.
This setting variable is primarily used in the Engine’s networking and replication subsystem. It is referenced in the DataReplication module, which is responsible for handling object replication across the network.
The value of this variable is set through a console variable (CVar) named “net.SupportFastArrayDelta”. It is initialized to 1 (enabled) by default, but can be changed at runtime through the console or configuration files.
The GSupportsFastArrayDelta variable interacts with the Fast Array Serialization system. When enabled, it allows for more efficient replication of array properties by tracking and sending only the changes (deltas) instead of the entire array.
Developers should be aware that this setting can significantly impact network performance and bandwidth usage. Enabling it can reduce network traffic for large arrays that frequently change, but it also introduces additional complexity in the replication process.
Best practices when using this variable include:
- Testing the performance impact with and without Fast Array Delta Serialization enabled for your specific use case.
- Ensuring that your FastArraySerializer implementations are correctly handling the delta serialization when this feature is enabled.
- Being cautious when disabling this feature, as it may increase network traffic for projects that rely on efficient array replication.
- Considering the trade-off between reduced network traffic and increased CPU usage for delta calculation.
- Monitoring network performance metrics to ensure the setting is providing the desired optimization for your specific game requirements.
#References in C++ code
#Callsites
This variable is referenced in the following C++ source code:
#Loc: <Workspace>/Engine/Source/Runtime/Engine/Private/DataReplication.cpp:64
Scope: file
Source code excerpt:
int32 GSupportsFastArrayDelta = 1;
static FAutoConsoleVariableRef CVarSupportsFastArrayDelta(
TEXT("net.SupportFastArrayDelta"),
GSupportsFastArrayDelta,
TEXT("Whether or not Fast Array Struct Delta Serialization is enabled.")
);
bool GbPushModelSkipUndirtiedReplicators = false;
FAutoConsoleVariableRef CVarPushModelSkipUndirtiedReplicators(
#Loc: <Workspace>/Engine/Source/Runtime/Engine/Private/DataReplication.cpp:844
Scope: file
Source code excerpt:
* replicate if anything has changed.
*
* When net.SupportFastArrayDelta is enabled, we perform an additional step in which we actually
* compare the properties of dirty items. This is very similar to normal Property replication
* using RepLayouts, and leverages most of the same code.
*
* This includes tracking history items just like Rep Layout. Instead of tracking histories per
* Sending Rep State / Per Connection, we just manage a single set of Histories on the Rep
* Changelist Mgr. Changelists are stored per Fast Array Item, and are referenced via ID.
*
* Whenever we go to replicate a Fast Array Item, we will merge together all changelists since
* we last sent that item, and send those accumulated changes.
*
* This means that property retransmission for Fast Array Items is an amalgamation of Rep Layout
* retransmission and CDP retransmission.
*
* Whenever a NAK is received, our History Number should be reset to the last known ACKed value,
* and that should be enough to force us to accumulate any of the NAKed item changelists.
*/
void FObjectReplicator::ReceivedNak( int32 NakPacketId )
{
const UObject* Object = GetObject();
if (Object == nullptr)
{
UE_LOG(LogNet, Verbose, TEXT("FObjectReplicator::ReceivedNak: Object == nullptr"));
return;
}
else if (ObjectClass == nullptr)
{
UE_LOG(LogNet, Verbose, TEXT("FObjectReplicator::ReceivedNak: ObjectClass == nullptr"));
}
else if (!RepLayout->IsEmpty())
{
if (FSendingRepState* SendingRepState = RepState.IsValid() ? RepState->GetSendingRepState() : nullptr)
{
SendingRepState->CustomDeltaChangeIndex--;
// Go over properties tracked with histories, and mark them as needing to be resent.
for (int32 i = SendingRepState->HistoryStart; i < SendingRepState->HistoryEnd; ++i)
{
const int32 HistoryIndex = i % FSendingRepState::MAX_CHANGE_HISTORY;
FRepChangedHistory& HistoryItem = SendingRepState->ChangeHistory[HistoryIndex];
if (!HistoryItem.Resend && HistoryItem.OutPacketIdRange.InRange(NakPacketId))
{
check(HistoryItem.Changed.Num() > 0);
HistoryItem.Resend = true;
++SendingRepState->NumNaks;
}
}
// Go over our Custom Delta Properties and update their retirements
for (int32 i = SendingRepState->Retirement.Num() - 1; i >= 0; i--)
{
FPropertyRetirement& Retirement = SendingRepState->Retirement[i];
ValidateRetirementHistory(Retirement, Object);
// If this is a dynamic array property, we have to look through the list of retirement records to see if we need to reset the base state
FPropertyRetirement* Rec = Retirement.Next; // Retirement[i] is head and not actually used in this case
uint32 LastAcknowledged = 0;
while (Rec != nullptr)
{
if (NakPacketId > Rec->OutPacketIdRange.Last)
{
// We can assume this means this record's packet was ack'd, so we can get rid of the old state
check(Retirement.Next == Rec);
Retirement.Next = Rec->Next;
LastAcknowledged = Rec->FastArrayChangelistHistory;
delete Rec;
Rec = Retirement.Next;
continue;
}
else if (NakPacketId >= Rec->OutPacketIdRange.First && NakPacketId <= Rec->OutPacketIdRange.Last)
{
UE_LOG(LogNet, VeryVerbose, TEXT("Restoring Previous Base State of dynamic property. Channel: %s, NakId: %d, First: %d, Last: %d, Address: %s)"), *OwningChannel->Describe(), NakPacketId, Rec->OutPacketIdRange.First, Rec->OutPacketIdRange.Last, *Connection->LowLevelGetRemoteAddress(true));
// The Nack'd packet did update this property, so we need to replace the buffer in RecentDynamic
// with the buffer we used to create this update (which was dropped), so that the update will be recreated on the next replicate actor
if (LastAcknowledged != 0 && Rec->DynamicState.IsValid())
{
Rec->DynamicState->SetLastAckedHistory(LastAcknowledged);
}
SendingRepState->RecentCustomDeltaState[i] = Rec->DynamicState;
// We can get rid of the rest of the saved off base states since we will be regenerating these updates on the next replicate actor
while (Rec != nullptr)
{
FPropertyRetirement * DeleteNext = Rec->Next;
delete Rec;
Rec = DeleteNext;
}
// Finished
Retirement.Next = nullptr;
break;
}
#Loc: <Workspace>/Engine/Source/Runtime/Net/Core/Classes/Net/Serialization/FastArraySerializer.h:208
Scope: file
Source code excerpt:
* struct to the last sent state, tracking changelists and only sending properties that changed exactly like the standard replication path.
* If this causes issues with a specific FastArray type, it can be disabled by calling FFastArraySerializer::SetDeltaSerializationEnabled(false) in the constructor.
* The feature can be completely disabled by setting the "net.SupportFastArrayDelta" CVar to 0.
*
* ReplicationID and ReplicationKeys are set by the MarkItemDirty function on FFastArraySerializer. These are just int32s that are assigned in order as things change.
* There is nothing special about them other than being unique.
*/
/**
* Helper to get auto-deduced FastArrayItemType from FastArraySerializer
*/
template <typename FastArrayType>
class TFastArrayTypeHelper
{
private:
struct CGetFastArrayItemTypeFuncable
{
template <typename InFastArrayType, typename...>
auto Requires(InFastArrayType* FastArray) -> decltype(FastArray->GetFastArrayItemTypePtr());
};
// Helper to always return a Type even if GetFastArrayItemTypePtr is not defined
static constexpr auto FastArrayTypePtr = []
{
if constexpr (TModels_V<CGetFastArrayItemTypeFuncable, FastArrayType>)
{
return FastArrayType::GetFastArrayItemTypePtr();
}
else
{
return static_cast<int*>(nullptr);
}
}();
public:
using FastArrayItemType = typename TRemovePointer<typename std::decay<decltype(FastArrayTypePtr)>::type>::Type;
/**
* Returns true if auto detected FastArrayItemType is a valid FastArrayItemType
*/
static constexpr bool HasValidFastArrayItemType() { return TIsDerivedFrom<FastArrayItemType, FFastArraySerializerItem>::IsDerived; }
};
/** Custom INetDeltaBaseState used by Fast Array Serialization */
class FNetFastTArrayBaseState : public INetDeltaBaseState
{
public:
FNetFastTArrayBaseState()
: ArrayReplicationKey(INDEX_NONE)
{}
virtual bool IsStateEqual(INetDeltaBaseState* OtherState)
{
FNetFastTArrayBaseState * Other = static_cast<FNetFastTArrayBaseState*>(OtherState);
for (auto It = IDToCLMap.CreateIterator(); It; ++It)
{
auto Ptr = Other->IDToCLMap.Find(It.Key());
if (!Ptr || *Ptr != It.Value())
{
return false;
}
}
return true;
}
virtual void CountBytes(FArchive& Ar) const override
{
IDToCLMap.CountBytes(Ar);
}
/** Maps an element's Replication ID to Index. */
TMap<int32, int32> IDToCLMap;
int32 ArrayReplicationKey;
};
/** Base struct for items using Fast TArray Replication */
USTRUCT()
struct FFastArraySerializerItem
{
GENERATED_USTRUCT_BODY()
FFastArraySerializerItem()
: ReplicationID(INDEX_NONE), ReplicationKey(INDEX_NONE), MostRecentArrayReplicationKey(INDEX_NONE)
{
}
FFastArraySerializerItem(const FFastArraySerializerItem &InItem)
: ReplicationID(INDEX_NONE), ReplicationKey(INDEX_NONE), MostRecentArrayReplicationKey(INDEX_NONE)
{
}