📉 Iris 网络复制系统技术分析 - 第十部分:增量压缩(Delta Compression)

🎯 本章目标
本章将深入剖析 Iris Delta Compression 的完整实现架构:
🗄️ Baseline 存储层:
DeltaCompressionBaselineStorage的 Reserve/Commit/Cancel 延迟克隆策略与引用计数机制🎛️ Baseline 管理层:
DeltaCompressionBaselineManager的生命周期管理、共享上下文、节流策略⚠️ 失效追踪:
BaselineInvalidationTracker如何与条件复制联动🔗 序列化链路:从
ReplicationWriter→ProtocolOps→StateOps→NetSerializer的完整调用栈✅ Ack/丢包处理:
ReplicationRecord如何驱动 baseline 状态机📊 性能模型:内存开销、CPU 开销、压缩率影响因素
10.1 🏗️ 架构概览:Delta Compression 的分层设计
10.1.1 📐 核心组件关系图
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ ReplicationWriter / ReplicationReader │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ SerializeObjectStateDelta() / DeserializeObjectStateDelta() ││
│ │ - 写/读 BaselineIndex (2 bits) ││
│ │ - 写/读 NewBaseline 标志 ││
│ │ - 调用 ProtocolOps::SerializeWithMaskDelta / DeserializeWithMaskDelta ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ReplicationProtocolOperations │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ SerializeWithMaskDelta(ChangeMask, SrcState, PrevState, Protocol) ││
│ │ - 写 ChangeMask (SparseBitArray) ││
│ │ - 遍历 ReplicationState,调用 StateOps::SerializeDeltaWithMask ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ReplicationStateOperations │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ SerializeDeltaWithMask(ChangeMask, Src, Prev, Descriptor) ││
│ │ - 遍历 Member,只序列化 ChangeMask 标记为脏的成员 ││
│ │ - 调用 NetSerializer::SerializeDelta(Source, Prev) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ NetSerializer │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ SerializeDelta(Context, {Source, Prev, ChangeMaskInfo}) ││
│ │ - 自定义 delta 逻辑 或 默认 delta (IsEqual ? skip : Serialize) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘10.1.2 🔄 Baseline 管理组件关系
PLAINTEXT
┌──────────────────────────────────────────────────────────────────────┐
│ DeltaCompressionBaselineManager │
│ - 对象级 Delta 启用状态 (DeltaCompressionEnabledObjects BitArray) │
│ - 连接级 Baseline 信息 (ObjectInfo->BaselinesForConnections) │
│ - Baseline 创建节流 (MinimumNumberOfFramesBetweenBaselines) │
│ - 同帧共享上下文 (BaselineSharingContext) │
└──────────────────────────────────────────────────────────────────────┘
│ 使用
▼
┌──────────────────────────────────────────────────────────────────────┐
│ DeltaCompressionBaselineStorage │
│ - StateBuffer 分配/释放 (通过 ReplicationStateStorage) │
│ - 引用计数管理 (RefCount) │
│ - Reserve/Commit/Cancel 延迟克隆 │
└──────────────────────────────────────────────────────────────────────┘
│ 使用
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ReplicationStateStorage │
│ - 实际内存分配 (FMemory::Malloc) │
│ - 状态克隆 (CloneState) │
│ - Baseline 预留/提交/取消 │
└──────────────────────────────────────────────────────────────────────┘10.2 🗄️ Baseline 存储层:DeltaCompressionBaselineStorage 深度剖析
10.2.1 📦 核心数据结构
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/DeltaCompression/DeltaCompressionBaselineStorage.h
// 对外暴露的 Baseline 状态信息class FDeltaCompressionBaselineStateInfo
{
public:
bool IsValid() const { return StateBuffer != nullptr; }
uint8* StateBuffer = nullptr; // 指向量化后的对象状态
DeltaCompressionBaselineStateInfoIndexType StateInfoIndex = InvalidDeltaCompressionBaselineStateInfoIndex;
};
// 内部存储结构(带引用计数)struct FInternalBaselineStateInfo
{
uint8* StateBuffer = nullptr;
uint32 ObjectIndex = 0;
uint32 RefCount = 1; // 关键:支持多连接共享同一 baseline
};设计要点:
💾
StateBuffer存储的是量化后的内部状态(Internal State),不是原始 UObject 属性🔢
RefCount实现多连接共享:同一帧内多个连接可能使用同一个 baseline,避免重复克隆🏷️
StateInfoIndex是 baseline 在存储数组中的索引,用于快速查找
10.2.2 ⏳ Reserve/Commit/Cancel:延迟克隆策略
这是 Iris 的一个精妙设计——不在创建 baseline 时立即克隆状态,而是延迟到确认需要时才克隆。
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/DeltaCompression/DeltaCompressionBaselineStorage.cpp
// 阶段1:Reserve - 预留但不克隆FDeltaCompressionBaselineStateInfo FDeltaCompressionBaselineStorage::ReserveBaselineForCurrentState(uint32 ObjectIndex){
FDeltaCompressionBaselineStateInfo BaselineStateInfo;
// 分配内部索引
const uint32 StateInfoIndex = AllocBaselineStateInfo();
FInternalBaselineStateInfo* InternalInfo = &BaselineStateInfos[StateInfoIndex];
InternalInfo->ObjectIndex = ObjectIndex;
InternalInfo->RefCount = 1;
// 关键:从 ReplicationStateStorage 获取"预留"
// 此时 StateBuffer 指向当前发送状态,尚未克隆
FReplicationStateStorage::FBaselineReservation Reservation =
ReplicationStateStorage->ReserveBaseline(ObjectIndex, EReplicationStateType::CurrentSendState);
InternalInfo->StateBuffer = const_cast<uint8*>(Reservation.BaselineBaseStorage);
BaselineStateInfo.StateBuffer = InternalInfo->StateBuffer;
BaselineStateInfo.StateInfoIndex = StateInfoIndex;
return BaselineStateInfo;
}
// 阶段2:根据引用计数决定 Commit 还是 Cancelvoid FDeltaCompressionBaselineStorage::OptionallyCommitAndDoReleaseBaseline(
DeltaCompressionBaselineStateInfoIndexType StateInfoIndex){
FInternalBaselineStateInfo* InternalInfo = &BaselineStateInfos[StateInfoIndex];
if (--InternalInfo->RefCount == 0)
{
// 没有连接需要这个 baseline 了,取消预留(不克隆)
ReplicationStateStorage->CancelBaselineReservation(
InternalInfo->ObjectIndex, InternalInfo->StateBuffer);
FreeBaselineStateInfo(StateInfoIndex);
}
else
{
// 还有连接需要,提交预留(执行克隆)
ReplicationStateStorage->CommitBaselineReservation(
InternalInfo->ObjectIndex,
InternalInfo->StateBuffer,
EReplicationStateType::CurrentSendState);
}
}为什么要这样设计? 🤔
考虑这个场景:服务器有 100 个连接,某对象需要创建 baseline。
❌ 朴素方案:立即克隆 100 份 → 100 次内存分配 + 100 次 memcpy
✅ Iris 方案:
Reserve 时只记录指针(指向当前状态)
同帧内其他连接共享同一个 Reserve(RefCount++)
帧末统一 Commit(只克隆 1 次)或 Cancel(0 次克隆)
10.2.3 💽 底层内存分配:ReplicationStateStorage
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationState/ReplicationStateStorage.cpp
uint8* FReplicationStateStorage::AllocBaseline(uint32 ObjectIndex, EReplicationStateType Base){
LLM_SCOPE_BYTAG(IrisState);
const FObjectInfo& ObjectInfo = ObjectInfos[ObjectIndex];
const FReplicationProtocol* Protocol = ObjectInfo.Protocol;
// 按协议定义的大小和对齐分配
uint8* Storage = static_cast<uint8*>(
FMemory::Malloc(Protocol->InternalTotalSize, Protocol->InternalTotalAlignment));
// 根据 Base 类型初始化
switch (Base)
{
case EReplicationStateType::UninitializedState:
// 不初始化,调用方负责填充
break;
case EReplicationStateType::ZeroedState:
FMemory::Memzero(Storage, Protocol->InternalTotalSize);
break;
case EReplicationStateType::DefaultState:
// 从 CDO 的量化状态克隆
CloneState(Protocol, Storage, Protocol->DefaultStateBuffer);
break;
case EReplicationStateType::CurrentSendState:
// 从当前发送状态克隆
CloneState(Protocol, Storage, ObjectInfo.StateBuffers[SendStateBufferIndex]);
break;
case EReplicationStateType::CurrentRecvState:
// 从当前接收状态克隆
CloneState(Protocol, Storage, ObjectInfo.StateBuffers[RecvStateBufferIndex]);
break;
}
return Storage;
}内存布局 📐:
每个 baseline 占用
Protocol->InternalTotalSize字节对齐要求:
Protocol->InternalTotalAlignment内容:所有
ReplicationState的量化成员按顺序排列
10.3 🎛️ Baseline 管理层:DeltaCompressionBaselineManager 深度剖析
10.3.1 🔒 核心设计约束
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/DeltaCompression/DeltaCompressionBaselineManager.h
class FDeltaCompressionBaselineManager
{
public:
// 硬编码约束:每个对象最多 2 个 baseline
enum : uint32 { MaxBaselineCount = 2 };
// InvalidBaselineIndex = 2,用 2 bits 编码 {0, 1, Invalid}
enum : uint32 { InvalidBaselineIndex = MaxBaselineCount };
enum : uint32 { BaselineIndexBitCount = 2 };
// ...
};为什么只有 2 个 baseline? 🤔
这是一个空间-时间权衡 ⚖️:
2 个 baseline 足以支持"滑动窗口"式的 Ack:发送 baseline[0],等 Ack;发送 baseline[1],等 Ack;Ack[0] 到了,释放 baseline[0],可以创建新的 baseline[0]...
更多 baseline 会增加内存开销,且实际收益有限(网络延迟通常不会导致需要 >2 个未确认 baseline)
10.3.2 📋 对象级数据结构
CPP
// DeltaCompressionBaselineManager.h
// 每个启用 Delta 压缩的对象的信息struct FObjectInfo
{
// 每个连接的 baseline 信息
TArray<FObjectBaselineInfo> BaselinesForConnections;
// 每个连接的 ChangeMask(自上次 baseline 以来的脏字段)
ChangeMaskStorageType* ChangeMasksForConnections = nullptr;
uint32 ChangeMaskStride = 0; // 每个连接的 ChangeMask 字数
// 节流控制
uint32 PrevBaselineCreationFrame = 0; // 上次创建 baseline 的帧号
};
// 每个连接的 baseline 信息struct FObjectBaselineInfo
{
FInternalBaseline Baselines[MaxBaselineCount]; // 最多 2 个 baseline
};
// 单个 baseline 的内部表示struct FInternalBaseline
{
bool IsValid() const { return BaselineStateInfoIndex != InvalidIndex; }
DeltaCompressionBaselineStateInfoIndexType BaselineStateInfoIndex = InvalidIndex;
ChangeMaskStorageType* ChangeMask = nullptr; // 该 baseline 创建时的 ChangeMask 快照
};10.3.3 🔧 CreateBaseline:完整流程
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/DeltaCompression/DeltaCompressionBaselineManager.cpp
FDeltaCompressionBaseline FDeltaCompressionBaselineManager::CreateBaseline(
uint32 ConnId, uint32 ObjectIndex, uint32 BaselineIndex){
FDeltaCompressionBaseline Baseline;
// 1. 检查对象是否启用 Delta 压缩
if (!DeltaCompressionEnabledObjects.GetBit(ObjectIndex))
{
return Baseline; // 返回无效 baseline
}
FObjectInfo* ObjectInfo = GetObjectInfo(ObjectIndex);
const uint32 ObjectInfoIndex = GetObjectInfoIndex(ObjectIndex);
FDeltaCompressionBaselineStateInfo BaselineStateInfo;
// 2. 检查是否可以共享已创建的 baseline(同帧优化)
if (BaselineSharingContext.ObjectInfoIndicesWithNewBaseline.GetBit(ObjectInfoIndex))
{
// 本帧已经为这个对象创建过 baseline,复用它
const auto ExistingStateInfoIndex =
BaselineSharingContext.ObjectInfoIndexToBaselineInfoIndex[ObjectInfoIndex];
BaselineStateInfo = BaselineStorage.GetBaselineReservationForCurrentState(ExistingStateInfoIndex);
}
else if (IsAllowedToCreateBaselineForObject(ConnId, ObjectIndex, ObjectInfo, ObjectInfoIndex))
{
// 3. 允许创建新 baseline
BaselineStateInfo = BaselineStorage.ReserveBaselineForCurrentState(ObjectIndex);
// 记录到共享上下文,供同帧其他连接复用
BaselineSharingContext.ObjectInfoIndicesWithNewBaseline.SetBit(ObjectInfoIndex);
BaselineSharingContext.ObjectInfoIndexToBaselineInfoIndex[ObjectInfoIndex] =
BaselineStateInfo.StateInfoIndex;
++BaselineSharingContext.CreatedBaselineCount;
// 更新节流计数器
ObjectInfo->PrevBaselineCreationFrame = FrameCounter;
}
else
{
// 4. 不允许创建(节流限制)
return Baseline;
}
// 5. 增加引用计数(该连接持有这个 baseline)
BaselineStorage.AddRefBaseline(BaselineStateInfo.StateInfoIndex);
// 6. 设置内部 baseline 信息
FObjectBaselineInfo& BaselineInfo = ObjectInfo->BaselinesForConnections[ConnId];
FInternalBaseline& InternalBaseline = BaselineInfo.Baselines[BaselineIndex];
InternalBaseline.BaselineStateInfoIndex = BaselineStateInfo.StateInfoIndex;
// 7. 关键:复制连接特定的 ChangeMask 到 baseline,并清零连接 ChangeMask
{
const uint32 ChangeMaskStride = ObjectInfo->ChangeMaskStride;
const SIZE_T ChangeMaskOffset = ChangeMaskStride * ConnId;
ChangeMaskStorageType* ConnectionChangeMask =
ObjectInfo->ChangeMasksForConnections + ChangeMaskOffset;
for (uint32 WordIt = 0; WordIt < ChangeMaskStride; ++WordIt)
{
InternalBaseline.ChangeMask[WordIt] = ConnectionChangeMask[WordIt];
ConnectionChangeMask[WordIt] = 0; // 清零,开始记录下一个周期的变化
}
}
// 8. 返回可用于序列化的 baseline
Baseline.ChangeMask = InternalBaseline.ChangeMask;
Baseline.StateBuffer = BaselineStateInfo.StateBuffer;
return Baseline;
}10.3.4 🚦 Baseline 创建节流策略
CPP
// DeltaCompressionBaselineManager.cpp
// CVar 控制static int32 MinimumNumberOfFramesBetweenBaselines = 60;
static FAutoConsoleVariableRef CVarMinimumNumberOfFramesBetweenBaselines(
TEXT("net.Iris.MinimumNumberOfFramesBetweenBaselines"),
MinimumNumberOfFramesBetweenBaselines,
TEXT("Minimum number of frames between creation of new delta compression baselines for an object. Default is 60.")
);
bool FDeltaCompressionBaselineManager::IsAllowedToCreateBaselineForObject(
uint32 ConnId, uint32 ObjectIndex,
const FObjectInfo* ObjectInfo, uint32 ObjectInfoIndex) const{
// 规则1:如果该连接没有任何 baseline,允许创建
const uint8 BaselineCount = BaselineCounts[ObjectInfoIndex * MaxConnectionCount + ConnId];
if (BaselineCount == 0)
{
return true;
}
// 规则2:检查帧间隔
const uint32 FramesSincePrevBaselineCreation =
FrameCounter - ObjectInfo->PrevBaselineCreationFrame;
if (FramesSincePrevBaselineCreation >= static_cast<uint32>(MinimumNumberOfFramesBetweenBaselines))
{
return true;
}
// 规则3:如果本帧已经为该对象创建过 baseline(被其他连接触发),允许共享
if (BaselineSharingContext.ObjectInfoIndicesWithNewBaseline.GetBit(ObjectInfoIndex))
{
return true;
}
return false;
}节流策略的意义 💡:
⏱️ 默认 60 帧(约 1 秒 @60fps)才能创建新 baseline
🛡️ 防止频繁创建 baseline 导致的 CPU/内存开销
🤝 同帧共享机制确保多连接场景不会重复创建
10.3.5 🤝 同帧共享上下文
CPP
// DeltaCompressionBaselineManager.h
struct FBaselineSharingContext
{
// 标记哪些对象在本帧已创建 baseline
FNetBitArray ObjectInfoIndicesWithNewBaseline;
// 对象索引 → baseline StateInfoIndex 的映射
TArray<DeltaCompressionBaselineStateInfoIndexType> ObjectInfoIndexToBaselineInfoIndex;
// 统计:本帧创建的 baseline 数量
uint32 CreatedBaselineCount = 0;
};工作流程 🔄:
帧开始时,
BeginBaselineSharingContext()清空上下文第一个连接请求创建 baseline → Reserve + 记录到上下文
后续连接请求同一对象的 baseline → 直接从上下文获取,AddRef
帧结束时,
EndBaselineSharingContext()执行 Commit/Cancel
10.4 ⚠️ Baseline 失效追踪:DeltaCompressionBaselineInvalidationTracker
10.4.1 ❓ 为什么需要失效追踪?
条件复制(Conditionals)是 baseline 失效的主要来源:
PLAINTEXT
场景:某字段 Health 配置为 COND_OwnerOnly 🏥
时刻 T1:PlayerA 是 Owner,baseline 包含 Health=100
时刻 T2:Owner 变更为 PlayerB
↓
问题 💥:PlayerA 的 baseline 中仍有 Health=100,但 PlayerA 现在不应该看到 Health
如果继续用这个 baseline 计算 delta,会导致状态错乱10.4.2 🔍 失效追踪器实现
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/DeltaCompression/DeltaCompressionBaselineInvalidationTracker.h
class FDeltaCompressionBaselineInvalidationTracker
{
public:
enum Constants : uint32
{
// 特殊值:使所有连接的 baseline 失效
InvalidateBaselineForAllConnections = 0U,
};
struct FInvalidationInfo
{
uint32 ConnId = InvalidateBaselineForAllConnections;
FInternalNetRefIndex ObjectIndex = 0U;
};
// 标记某对象的 baseline 需要失效
void InvalidateBaselines(FInternalNetRefIndex ObjectIndex, uint32 ConnId);
// 获取待处理的失效列表
TArrayView<const FInvalidationInfo> GetBaselineInvalidationInfos() const;
private:
FNetBitArray InvalidatedObjects; // 避免重复添加
TArray<FInvalidationInfo> InvalidationInfos;
};
// DeltaCompressionBaselineInvalidationTracker.cpp
void FDeltaCompressionBaselineInvalidationTracker::InvalidateBaselines(
FInternalNetRefIndex ObjectIndex, uint32 ConnId){
// 避免重复处理
if (InvalidatedObjects.GetBit(ObjectIndex))
{
return;
}
// 检查对象是否启用 Delta 压缩
if (BaselineManager->GetDeltaCompressionStatus(ObjectIndex) != ENetObjectDeltaCompressionStatus::Allow)
{
return;
}
if (ConnId == InvalidateBaselineForAllConnections)
{
InvalidatedObjects.SetBit(ObjectIndex);
}
InvalidationInfos.Emplace(FInvalidationInfo{ConnId, ObjectIndex});
}10.4.3 🔗 条件复制触发失效
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/Conditionals/ReplicationConditionals.cpp
void FReplicationConditionals::InvalidateBaselinesForObjectHierarchy(
uint32 ObjectIndex,
const TConstArrayView<uint32>& ConnectionsToInvalidate){
// 1. 处理根对象
{
const FNetRefHandleManager::FReplicatedObjectData& ObjectData =
NetRefHandleManager->GetReplicatedObjectDataNoCheck(ObjectIndex);
// 只有带生命周期条件的对象才需要失效
if (EnumHasAnyFlags(ObjectData.Protocol->ProtocolTraits,
EReplicationProtocolTraits::HasLifetimeConditionals))
{
for (const uint32 ConnId : ConnectionsToInvalidate)
{
BaselineInvalidationTracker->InvalidateBaselines(ObjectIndex, ConnId);
}
}
}
// 2. 递归处理子对象
for (const FInternalNetRefIndex SubObjectIndex : NetRefHandleManager->GetSubObjects(ObjectIndex))
{
const FNetRefHandleManager::FReplicatedObjectData& SubObjectData =
NetRefHandleManager->GetReplicatedObjectDataNoCheck(SubObjectIndex);
if (EnumHasAnyFlags(SubObjectData.Protocol->ProtocolTraits,
EReplicationProtocolTraits::HasLifetimeConditionals))
{
for (const uint32 ConnId : ConnectionsToInvalidate)
{
BaselineInvalidationTracker->InvalidateBaselines(SubObjectIndex, ConnId);
}
}
}
}10.4.4 🧹 失效处理
CPP
// DeltaCompressionBaselineManager.cpp
void FDeltaCompressionBaselineManager::InvalidateBaselinesDueToModifiedConditionals(){
for (const FInvalidationInfo& Info : BaselineInvalidationTracker->GetBaselineInvalidationInfos())
{
FObjectInfo* ObjectInfo = GetObjectInfo(Info.ObjectIndex);
if (Info.ConnId == InvalidateBaselineForAllConnections)
{
// 使所有连接的 baseline 失效
for (uint32 ConnectionId : ValidConnections)
{
InvalidateBaselinesForConnection(ObjectInfo, ConnectionId);
}
}
else
{
// 只使特定连接的 baseline 失效
InvalidateBaselinesForConnection(ObjectInfo, Info.ConnId);
}
}
BaselineInvalidationTracker->Reset();
}
void FDeltaCompressionBaselineManager::InvalidateBaselinesForConnection(
FObjectInfo* ObjectInfo, uint32 ConnId){
FObjectBaselineInfo& BaselineInfo = ObjectInfo->BaselinesForConnections[ConnId];
for (uint32 BaselineIndex = 0; BaselineIndex < MaxBaselineCount; ++BaselineIndex)
{
FInternalBaseline& InternalBaseline = BaselineInfo.Baselines[BaselineIndex];
if (InternalBaseline.IsValid())
{
// 释放 baseline,但保留 ChangeMask(合并回连接 ChangeMask)
ReleaseInternalBaseline(InternalBaseline, EChangeMaskBehavior::Merge);
}
}
}10.5 🔗 序列化链路:从 Writer 到 NetSerializer 的完整调用栈
10.5.1 📤 发送侧:ReplicationWriter::SerializeObjectStateDelta
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationWriter.cpp
void FReplicationWriter::SerializeObjectStateDelta(
FNetSerializationContext& Context,
const FNetRefHandleManager::FReplicatedObjectData& ObjectData,
FReplicationInfo& Info,
FDeltaCompressionBaseline& CurrentBaseline,
uint32 CreatedBaselineIndex){
FNetBitStreamWriter& Writer = *Context.GetBitStreamWriter();
// 1. 写 BaselineIndex (2 bits)
// 告诉客户端:我基于哪个 baseline 计算的 delta
Writer.WriteBits(Info.LastAckedBaselineIndex, FDeltaCompressionBaselineManager::BaselineIndexBitCount);
if (Info.LastAckedBaselineIndex != FDeltaCompressionBaselineManager::InvalidBaselineIndex)
{
// 2a. 有有效 baseline,走 delta 路径
// 写 NewBaseline 标志:本包是否携带新 baseline
const bool bHasNewBaseline = (CreatedBaselineIndex != FDeltaCompressionBaselineManager::InvalidBaselineIndex);
Writer.WriteBool(bHasNewBaseline);
// 调用协议级 delta 序列化
FReplicationProtocolOperations::SerializeWithMaskDelta(
Context,
Info.GetChangeMaskStoragePointer(),
ReplicatedObjectStateBuffer, // 当前状态
CurrentBaseline.StateBuffer, // baseline 状态
ObjectData.Protocol);
}
else
{
// 2b. 无有效 baseline,走全量路径
// 写 NewBaselineIndex:告诉客户端存储这个作为新 baseline
Writer.WriteBits(CreatedBaselineIndex, FDeltaCompressionBaselineManager::BaselineIndexBitCount);
// 调用普通序列化(非 delta)
FReplicationProtocolOperations::SerializeWithMask(
Context,
Info.GetChangeMaskStoragePointer(),
ReplicatedObjectStateBuffer,
ObjectData.Protocol);
}
}10.5.2 📜 协议级:SerializeWithMaskDelta
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationOperations.cpp
void FReplicationProtocolOperations::SerializeWithMaskDelta(
FNetSerializationContext& Context,
const uint32* ChangeMaskData,
const uint8* SrcObjectStateBuffer,
const uint8* PrevObjectStateBuffer,
const FReplicationProtocol* Protocol){
FNetBitStreamWriter& Writer = *Context.GetBitStreamWriter();
// 1. 写 ChangeMask(使用稀疏位数组格式压缩)
WriteSparseBitArray(&Writer, ChangeMaskData, Protocol->ChangeMaskBitCount);
// 2. 准备 ChangeMask 视图
const FNetBitArrayView ChangeMask = MakeNetBitArrayView(ChangeMaskData, Protocol->ChangeMaskBitCount);
// 3. 遍历所有 ReplicationState
const FReplicationStateDescriptor* const* ReplicationStateDescriptors = Protocol->ReplicationStateDescriptors;
uint32 CurrentChangeMaskBitOffset = 0;
for (uint32 StateIt = 0; StateIt < Protocol->ReplicationStateCount; ++StateIt)
{
const FReplicationStateDescriptor* CurrentDescriptor = ReplicationStateDescriptors[StateIt];
// 计算当前状态在 buffer 中的偏移
const uint8* CurrentInternalStateBuffer = SrcObjectStateBuffer + CurrentDescriptor->ExternalOffset;
const uint8* PrevInternalStateBuffer = PrevObjectStateBuffer + CurrentDescriptor->ExternalOffset;
if (CurrentDescriptor->IsInitState())
{
// InitState:完整 delta 序列化(不使用 ChangeMask)
FReplicationStateOperations::SerializeDelta(
Context,
CurrentInternalStateBuffer,
PrevInternalStateBuffer,
CurrentDescriptor);
}
else
{
// 普通 State:带 Mask 的 delta 序列化
FReplicationStateOperations::SerializeDeltaWithMask(
Context,
ChangeMask,
CurrentChangeMaskBitOffset,
CurrentInternalStateBuffer,
PrevInternalStateBuffer,
CurrentDescriptor);
}
CurrentChangeMaskBitOffset += CurrentDescriptor->ChangeMaskBitCount;
}
}10.5.3 📊 状态级:SerializeDeltaWithMask
CPP
// ReplicationOperations.cpp
void FReplicationStateOperations::SerializeDeltaWithMask(
FNetSerializationContext& Context,
const FNetBitArrayView& ChangeMask,
const uint32 ChangeMaskOffset,
const uint8* RESTRICT SrcInternalBuffer,
const uint8* RESTRICT PrevInternalBuffer,
const FReplicationStateDescriptor* Descriptor){
const FReplicationStateMemberDescriptor* MemberDescriptors = Descriptor->MemberDescriptors;
const FReplicationStateMemberSerializerDescriptor* MemberSerializerDescriptors =
Descriptor->MemberSerializerDescriptors;
const FReplicationStateMemberChangeMaskDescriptor* MemberChangeMaskDescriptors =
Descriptor->MemberChangeMaskDescriptors;
const uint32 MemberCount = Descriptor->MemberCount;
for (uint32 MemberIt = 0; MemberIt < MemberCount; ++MemberIt)
{
const FReplicationStateMemberDescriptor& MemberDescriptor = MemberDescriptors[MemberIt];
const FReplicationStateMemberSerializerDescriptor& MemberSerializerDescriptor =
MemberSerializerDescriptors[MemberIt];
const FReplicationStateMemberChangeMaskDescriptor& MemberChangeMaskDescriptor =
MemberChangeMaskDescriptors[MemberIt];
// 计算该成员在 ChangeMask 中的位置
const uint32 MemberChangeMaskOffset = ChangeMaskOffset + MemberChangeMaskDescriptor.BitOffset;
// 关键:只序列化 ChangeMask 中标记为脏的成员
if (ChangeMask.IsAnyBitSet(MemberChangeMaskOffset, MemberChangeMaskDescriptor.BitCount))
{
FNetSerializeDeltaArgs Args;
Args.Version = 0;
Args.NetSerializerConfig = MemberSerializerDescriptor.SerializerConfig;
Args.Source = NetSerializerValuePointer(SrcInternalBuffer + MemberDescriptor.InternalMemberOffset);
Args.Prev = NetSerializerValuePointer(PrevInternalBuffer + MemberDescriptor.InternalMemberOffset);
Args.ChangeMaskInfo.BitCount = MemberChangeMaskDescriptor.BitCount;
Args.ChangeMaskInfo.BitOffset = MemberChangeMaskOffset;
// 调用 NetSerializer 的 SerializeDelta
MemberSerializerDescriptor.Serializer->SerializeDelta(Context, Args);
}
}
}10.5.4 🎯 字段级:NetSerializer::SerializeDelta
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Serialization/NetSerializer.h
struct FNetSerializeDeltaArgs : FNetSerializeArgs
{
// 指向上一个已确认的量化数据(baseline)
NetSerializerValuePointer Prev;
};
// 典型的自定义 delta 实现(以 QuantizedVector 为例)static void SerializeDelta(FNetSerializationContext& Context, const FNetSerializeDeltaArgs& Args){
const QuantizedType& Value = *reinterpret_cast<const QuantizedType*>(Args.Source);
const QuantizedType& PrevValue = *reinterpret_cast<const QuantizedType*>(Args.Prev);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 计算差值
const int32 DeltaX = Value.X - PrevValue.X;
const int32 DeltaY = Value.Y - PrevValue.Y;
const int32 DeltaZ = Value.Z - PrevValue.Z;
// 写差值(通常用更少的位数)
Writer->WriteBits(DeltaX, DeltaBitCount);
Writer->WriteBits(DeltaY, DeltaBitCount);
Writer->WriteBits(DeltaZ, DeltaBitCount);
}10.5.5 🔄 默认 Delta 实现
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Serialization/NetSerializerBuilder.inl
// 如果 Serializer 没有实现 SerializeDelta,且满足条件,使用默认实现template<typename T = void,
typename U = typename TEnableIf<!HasSerializeDelta && ShouldUseDefaultDelta() && HasIsEqual, T>::Type,
char V = 0>
static NetSerializeDeltaFunction GetSerializeDeltaFunction()
{
return NetSerializeDeltaDefault<NetSerializerImpl::Serialize, NetSerializerImpl::IsEqual>;
}
// 默认 delta 实现template<NetSerializeFunction SerializeFunc, NetIsEqualFunction IsEqualFunc>
void NetSerializeDeltaDefault(FNetSerializationContext& Context, const FNetSerializeDeltaArgs& Args){
// 如果值相等,写 1 bit (0) 表示"无变化"
// 如果值不等,写 1 bit (1) + 完整序列化
FNetIsEqualArgs IsEqualArgs;
IsEqualArgs.Source0 = Args.Source;
IsEqualArgs.Source1 = Args.Prev;
IsEqualArgs.NetSerializerConfig = Args.NetSerializerConfig;
const bool bIsEqual = IsEqualFunc(Context, IsEqualArgs);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
Writer->WriteBool(!bIsEqual);
if (!bIsEqual)
{
// 值不同,退化为完整序列化
FNetSerializeArgs SerializeArgs;
SerializeArgs.Source = Args.Source;
SerializeArgs.NetSerializerConfig = Args.NetSerializerConfig;
SerializeFunc(Context, SerializeArgs);
}
}默认 delta 的局限性 ⚠️:
❌ 只能做"相等则跳过,不等则全量"
❌ 无法利用数值接近性(如位置小幅变化)
❌ 对于复杂类型(数组、结构体),可能不如自定义 delta 高效
10.6 📥 接收侧:ReplicationReader 的 Delta 反序列化
10.6.1 🔍 DeserializeObjectStateDelta
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationReader.cpp
void FReplicationReader::DeserializeObjectStateDelta(
FNetSerializationContext& Context,
const FNetRefHandleManager::FReplicatedObjectData& ObjectData,
FReplicationInfo& Info,
FObjectReferenceCache& ObjectReferenceCache,
uint32& OutNewBaselineIndex){
FNetBitStreamReader& Reader = *Context.GetBitStreamReader();
// 1. 读 BaselineIndex
const uint32 BaselineIndex = Reader.ReadBits(FDeltaCompressionBaselineManager::BaselineIndexBitCount);
if (BaselineIndex != FDeltaCompressionBaselineManager::InvalidBaselineIndex)
{
// 2a. 有效 baseline,走 delta 路径
// 读 NewBaseline 标志
const bool bIsNewBaseline = Reader.ReadBool();
OutNewBaselineIndex = bIsNewBaseline ? BaselineIndex : InvalidBaselineIndex;
// 获取本地存储的 baseline
const uint8* StoredBaseline = ObjectInfo.StoredBaselines[BaselineIndex];
// 调用协议级 delta 反序列化
FReplicationProtocolOperations::DeserializeWithMaskDelta(
Context,
Info.ChangeMaskOrPointer.GetPointer(ObjectInfo.ChangeMaskBitCount),
ObjectData.ReceiveStateBuffer, // 目标:接收状态 buffer
StoredBaseline, // 参照:存储的 baseline
ObjectData.Protocol);
}
else
{
// 2b. 无效 baseline,走全量路径
// 读 NewBaselineIndex
OutNewBaselineIndex = Reader.ReadBits(FDeltaCompressionBaselineManager::BaselineIndexBitCount);
// 调用普通反序列化
FReplicationProtocolOperations::DeserializeWithMask(
Context,
Info.ChangeMaskOrPointer.GetPointer(ObjectInfo.ChangeMaskBitCount),
ObjectData.ReceiveStateBuffer,
ObjectData.Protocol);
}
}10.6.2 💾 客户端 Baseline 存储
CPP
// ReplicationReader.h
struct FObjectInfo
{
// 存储的 baseline(最多 2 个)
uint8* StoredBaselines[FDeltaCompressionBaselineManager::MaxBaselineCount] = {nullptr, nullptr};
// ...
};
// 收到新 baseline 后存储void FReplicationReader::StoreBaseline(FObjectInfo& ObjectInfo, uint32 BaselineIndex, const uint8* StateBuffer){
// 如果已有旧 baseline,释放
if (ObjectInfo.StoredBaselines[BaselineIndex] != nullptr)
{
FMemory::Free(ObjectInfo.StoredBaselines[BaselineIndex]);
}
// 分配新存储并克隆
const uint32 StateSize = ObjectInfo.Protocol->InternalTotalSize;
ObjectInfo.StoredBaselines[BaselineIndex] = static_cast<uint8*>(
FMemory::Malloc(StateSize, ObjectInfo.Protocol->InternalTotalAlignment));
FMemory::Memcpy(ObjectInfo.StoredBaselines[BaselineIndex], StateBuffer, StateSize);
}10.7 ✅ Ack 与丢包处理:Baseline 状态机
10.7.1 📝 ReplicationRecord:记录发送历史
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationRecord.h
struct FReplicationRecord
{
struct FRecordInfo
{
// 本次发送使用的 baseline index
uint32 BaselineIndex : 2;
// 本次发送创建的新 baseline index(如果有)
uint32 NewBaselineIndex : 2;
// 其他信息...
};
// 每个对象的发送记录
TArray<FRecordInfo> RecordInfos;
};10.7.2 ✅ Ack 处理:HandleDeliveredRecord
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationWriter.cpp
void FReplicationWriter::HandleDeliveredRecord(
const FReplicationRecord::FRecordInfo& RecordInfo,
FReplicationInfo& Info,
uint32 InternalIndex){
// 1. 确认新 baseline
if (RecordInfo.NewBaselineIndex != FDeltaCompressionBaselineManager::InvalidBaselineIndex)
{
check(RecordInfo.NewBaselineIndex == Info.PendingBaselineIndex);
// 销毁旧的已确认 baseline
if (Info.LastAckedBaselineIndex != InvalidBaselineIndex)
{
BaselineManager->DestroyBaseline(
Parameters.ConnectionId,
InternalIndex,
Info.LastAckedBaselineIndex);
}
// 更新状态:PendingBaseline → LastAckedBaseline
Info.LastAckedBaselineIndex = RecordInfo.NewBaselineIndex;
Info.PendingBaselineIndex = InvalidBaselineIndex;
}
}10.7.3 ❌ 丢包处理:HandleDroppedRecord
CPP
// ReplicationWriter.cpp
void FReplicationWriter::HandleDroppedRecord(
const FReplicationRecord::FRecordInfo& RecordInfo,
FReplicationInfo& Info,
uint32 InternalIndex){
// 1. 处理丢失的新 baseline
if (RecordInfo.NewBaselineIndex != FDeltaCompressionBaselineManager::InvalidBaselineIndex)
{
// 关键:使用 LostBaseline 而不是 DestroyBaseline
// LostBaseline 会将 baseline 的 ChangeMask 合并回连接 ChangeMask
// 确保丢失的数据在下次发送时重新发送
BaselineManager->LostBaseline(
Parameters.ConnectionId,
InternalIndex,
RecordInfo.NewBaselineIndex);
Info.PendingBaselineIndex = InvalidBaselineIndex;
}
}10.7.4 ⚖️ Lost vs Destroy:ChangeMask 处理差异
CPP
// DeltaCompressionBaselineManager.cpp
void FDeltaCompressionBaselineManager::LostBaseline(uint32 ConnId, uint32 ObjectIndex, uint32 BaselineIndex){
// 丢包:合并 ChangeMask
DestroyBaseline(ConnId, ObjectIndex, BaselineIndex, EChangeMaskBehavior::Merge);
}
void FDeltaCompressionBaselineManager::DestroyBaseline(uint32 ConnId, uint32 ObjectIndex, uint32 BaselineIndex){
// 正常销毁:丢弃 ChangeMask
DestroyBaseline(ConnId, ObjectIndex, BaselineIndex, EChangeMaskBehavior::Discard);
}
void FDeltaCompressionBaselineManager::DestroyBaseline(
uint32 ConnId, uint32 ObjectIndex, uint32 BaselineIndex,
EChangeMaskBehavior ChangeMaskBehavior){
FObjectInfo* ObjectInfo = GetObjectInfo(ObjectIndex);
FInternalBaseline& InternalBaseline = ObjectInfo->BaselinesForConnections[ConnId].Baselines[BaselineIndex];
if (ChangeMaskBehavior == EChangeMaskBehavior::Merge)
{
// 将 baseline 的 ChangeMask 合并回连接 ChangeMask
const uint32 ChangeMaskStride = ObjectInfo->ChangeMaskStride;
ChangeMaskStorageType* ConnectionChangeMask =
ObjectInfo->ChangeMasksForConnections + ChangeMaskStride * ConnId;
for (uint32 WordIt = 0; WordIt < ChangeMaskStride; ++WordIt)
{
ConnectionChangeMask[WordIt] |= InternalBaseline.ChangeMask[WordIt];
}
}
ReleaseInternalBaseline(InternalBaseline);
}为什么丢包要合并 ChangeMask? 🤔
PLAINTEXT
时刻 T1:发送 baseline[0],包含字段 {A, B, C} 📦
时刻 T2:字段 D 变化,记录到连接 ChangeMask ✏️
时刻 T3:baseline[0] 的包丢失 ❌
如果不合并:下次发送只会发 {D},客户端缺少 {A, B, C} 💔
如果合并:下次发送会发 {A, B, C, D},客户端状态完整 ✅10.8 🆕 初始状态 Delta 压缩
10.8.1 🔄 与 Default State 做 Delta
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationOperations.cpp
static bool bDeltaCompressInitialState = true;
static FAutoConsoleVariableRef CVarbDeltaCompressInitialState(
TEXT("net.Iris.DeltaCompressInitialState"),
bDeltaCompressInitialState,
TEXT("if true we compare with default state when serializing initial state.")
);
void FReplicationProtocolOperations::SerializeInitialStateWithMask(
FNetSerializationContext& Context,
const uint32* ChangeMaskData,
const uint8* SrcObjectStateBuffer,
const FReplicationProtocol* Protocol){
if (!bDeltaCompressInitialState)
{
// 禁用时使用普通序列化
SerializeWithMask(Context, ChangeMaskData, SrcObjectStateBuffer, Protocol);
return;
}
FNetBitStreamWriter& Writer = *Context.GetBitStreamWriter();
// 写 ChangeMask(初始状态通常大部分字段都是脏的,使用 ContainsMostlyOnes 提示)
WriteSparseBitArray(&Writer, ChangeMaskData, Protocol->ChangeMaskBitCount,
ESparseBitArraySerializationHint::ContainsMostlyOnes);
// 遍历 ReplicationState
for (uint32 StateIt = 0; StateIt < Protocol->ReplicationStateCount; ++StateIt)
{
const FReplicationStateDescriptor* CurrentDescriptor = ReplicationStateDescriptors[StateIt];
const uint8* CurrentInternalStateBuffer = SrcObjectStateBuffer + CurrentDescriptor->ExternalOffset;
// 关键:与 DefaultStateBuffer 做 delta
const uint8* DefaultStateBuffer = CurrentDescriptor->DefaultStateBuffer;
if (CurrentDescriptor->IsInitState())
{
FReplicationStateOperations::SerializeDelta(
Context, CurrentInternalStateBuffer, DefaultStateBuffer, CurrentDescriptor);
}
else
{
FReplicationStateOperations::SerializeDeltaWithMask(
Context, ChangeMask, CurrentChangeMaskBitOffset,
CurrentInternalStateBuffer, DefaultStateBuffer, CurrentDescriptor);
}
}
}10.8.2 📦 Default State 的来源
CPP
// FReplicationStateDescriptor 在构建时从 CDO 量化得到 DefaultStateBuffer
// ReplicationStateDescriptorBuilder.cpp(简化)void BuildDescriptor(UClass* Class, FReplicationStateDescriptor* Descriptor){
// 获取 CDO
UObject* CDO = Class->GetDefaultObject();
// 分配 DefaultStateBuffer
Descriptor->DefaultStateBuffer = FMemory::Malloc(Descriptor->InternalSize, Descriptor->InternalAlignment);
// 将 CDO 的属性量化到 DefaultStateBuffer
QuantizeObjectState(CDO, Descriptor->DefaultStateBuffer, Descriptor);
}10.9 📊 性能模型与优化建议
10.9.1 💾 内存开销模型
PLAINTEXT
每个启用 Delta 的对象的内存开销:
Server 侧:
- FObjectInfo:固定开销 ~64 bytes
- BaselinesForConnections:MaxConnectionCount × MaxBaselineCount × sizeof(FInternalBaseline)
= 100 × 2 × 32 = 6.4 KB(假设 100 连接)
- ChangeMasksForConnections:MaxConnectionCount × ChangeMaskStride × 4
= 100 × 4 × 4 = 1.6 KB(假设 128 个可变字段)
- Baseline StateBuffer:RefCount × Protocol->InternalTotalSize
= 1~2 × 512 = 0.5~1 KB(假设 512 bytes 状态)
单对象总计:~8-10 KB
Client 侧:
- StoredBaselines:MaxBaselineCount × Protocol->InternalTotalSize
= 2 × 512 = 1 KB
单对象总计:~1 KB
10.9.2 ⚡ CPU 开销分析
PLAINTEXT
发送侧(每帧每对象):
1. CreateBaseline:O(ChangeMaskStride) - ChangeMask 复制
2. SerializeWithMaskDelta:O(MemberCount) - 遍历成员
3. 每个脏成员:SerializeDelta 开销取决于 serializer 实现
接收侧(每帧每对象):
1. DeserializeWithMaskDelta:O(MemberCount)
2. 每个脏成员:DeserializeDelta
3. StoreBaseline(如果有新 baseline):O(StateSize) memcpy10.9.3 📉 压缩率影响因素
因素 | 影响 | 建议 |
|---|---|---|
🎯 脏字段比例 | 脏字段越少,压缩率越高 | 减少不必要的 SetDirty |
📏 字段值变化幅度 | 小幅变化可用更少 bits | 使用自定义 delta serializer |
⏰ Baseline 新鲜度 | 越新鲜,delta 越小 | 适当降低 MinimumNumberOfFramesBetweenBaselines |
🔢 ChangeMask 稀疏度 | 越稀疏,ChangeMask 压缩越好 | 将相关字段放在一起 |
10.9.4 💡 优化建议
1. 🎯 选择合适的对象启用 Delta
INI
; ✅ 推荐:字段多但变化少的对象
+DeltaCompressionConfigs=(ClassName=/Script/Engine.Pawn)
+DeltaCompressionConfigs=(ClassName=/Script/Engine.PlayerState)
; ❌ 不推荐:字段少或几乎每帧全变的对象; ❌ 不推荐:生命周期极短的对象2. ⏱️ 调整 Baseline 创建频率
CPP
// 🎮 变化平滑的游戏:增大间隔
net.Iris.MinimumNumberOfFramesBetweenBaselines 120
// 🚀 频繁瞬移/传送的游戏:减小间隔
net.Iris.MinimumNumberOfFramesBetweenBaselines 303. ✨ 自定义高效 Delta Serializer
CPP
// 对于位置等连续变化的数据,使用差值编码static void SerializeDelta(FNetSerializationContext& Context, const FNetSerializeDeltaArgs& Args){
const FVector& Value = *reinterpret_cast<const FVector*>(Args.Source);
const FVector& Prev = *reinterpret_cast<const FVector*>(Args.Prev);
// 计算差值,使用更少的位数
FVector Delta = Value - Prev;
// 如果差值在小范围内,用更少的位数
if (FMath::Abs(Delta.X) < 256 && FMath::Abs(Delta.Y) < 256 && FMath::Abs(Delta.Z) < 256)
{
Writer->WriteBool(true); // 小范围标志
Writer->WriteBits(Delta.X + 128, 8);
Writer->WriteBits(Delta.Y + 128, 8);
Writer->WriteBits(Delta.Z + 128, 8);
}
else
{
Writer->WriteBool(false);
// 完整写入
Serialize(Context, Args);
}
}10.10 🔧 调试与排查
10.10.1 🎛️ 关键 CVars
CVar | 默认值 | 说明 |
|---|---|---|
| true | 总开关 |
| 60 | Baseline 创建最小帧间隔 |
| true | 初始状态是否与 default 做 delta |
10.10.2 🔍 调试技巧
1. ✅ 确认 Delta 路径是否生效
CPP
// ReplicationReader.cpp 中有 trace scopeUE_NET_TRACE_SCOPE(DeltaCompressed, *Context.GetBitStreamReader(), Context.GetTraceCollector(), ENetTraceVerbosity::Trace);
// 使用 Network Insights 或 Unreal Insights 查看2. 📋 检查 Baseline 状态
CPP
// 在 BaselineManager 中添加日志UE_LOG(LogIris, Log, TEXT("CreateBaseline: Object=%d, Conn=%d, Index=%d"),
ObjectIndex, ConnId, BaselineIndex);3. 💾 监控内存使用
CPP
// LLM 标签LLM_SCOPE_BYTAG(IrisState);
// 使用 Unreal Insights 的 LLM 视图查看 IrisState 内存10.10.3 🚨 常见问题排查
问题 | 可能原因 | 排查方法 |
|---|---|---|
❌ 不走 delta | 总开关关闭 | 检查 |
❌ 不走 delta | 协议不支持 | 检查 |
❌ 不走 delta | 类未配置 | 检查 |
📉 压缩率低 | 脏字段太多 | 减少不必要的属性变化 |
💥 状态错乱 | Baseline 失效未处理 | 检查 Conditionals 变化是否触发 invalidation |
💾 内存高 | 对象太多 | 降低 |
10.11 📚 关键源文件索引
功能模块 | 文件路径 |
|---|---|
🗄️ Baseline 存储 |
|
🎛️ Baseline 管理 |
|
⚠️ 失效追踪 |
|
💾 状态存储 |
|
📤 发送侧序列化 |
|
📥 接收侧反序列化 |
|
📜 协议级操作 |
|
📝 发送记录 |
|
🎯 NetSerializer 接口 |
|
🔄 默认 Delta 实现 |
|
本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)