MENU

Unreal Iris Replication 性能初测

March 10, 2024 • 程序阅读设置

前言

众所周知,Epic 在 Unreal Engine 5.1 版本开始以实验特性发布了名为 Iris 的全新复制(Replication)系统。Iris 可以说是对传统复制系统(下称“Legacy”)的全面重做,官方表示这提高了性能、伸缩性、可维护性等。代码量在60,000行左右。可贵的是新的系统保持了对之前各类 game framework 概念的兼容。

目前在互联网上,无论是哪种语言编写的 Iris 相关内容都比较少。(这也是 UE 的特点,有了源码还要什么资料?)但本文不打算对 Iris 的概念和实现等进行阐述,仅对项目集成和性能测试等展开。使用源码来自官方 GitHub: EpicGames/UnrealEngine,版本 5.3.2(注意该版已与部分基于 5.1 的文章描述有出入,迭代较快,细节请自行确认)。

项目集成

本文为了便于测试,以官方的 ThirdPerson 示例为基础,创建一个新工程,名为 IrisDemo。后面均默认工程名如此。

第一步是将 Iris 插件引入,即添加到 .uprojectPlugin 部分。这与其他插件的引入没有区别。

{
    "FileVersion": 3,
    "EngineAssociation": "{7020973D-4646-B1A9-47F2-B7BCEA27C9E7}",
    "Category": "",
    "Description": "",
    "Modules": [
        {
            "Name": "IrisDemo",
            "Type": "Runtime",
            "LoadingPhase": "Default"
        }
    ],
    "Plugins": [
        {
            "Name": "ModelingToolsEditorMode",
            "Enabled": true,
            "TargetAllowList": [
                "Editor"
            ]
        },
        {
            "Name": "Iris",
            "Enabled": true
        }
    ],
    "TargetPlatforms": [
        "Windows"
    ]
}

第二步是修改 .Build.cs,加上 SetupIrisSupport,构建时才会进行必须的操作。

using UnrealBuildTool;

public class IrisDemo : ModuleRules
{
    public IrisDemo(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
        PublicDependencyModuleNames.AddRange(new []
        {
            "Core", 
            "CoreUObject", 
            "Engine",
            "NetCore",
            "InputCore", 
            "EnhancedInput"
        });
        SetupIrisSupport(Target);
    }
}

第三步是向项目的 DefaultEngine.ini 添加 Iris 相关的配置,大致如下:

[/Script/Engine.Engine]
+IrisNetDriverConfigs=(NetDriverDefinition="GameNetDriver", bCanUseIris=true)

[/Script/Engine.NetDriver]
+ChannelDefinitions=(ChannelName=DataStream, ClassName=/Script/Engine.DataStreamChannel, StaticChannelIndex=2, bTickOnCreate=true, bServerOpen=true, bClientOpen=true, bInitialServer=true, bInitialClient=true)

[SystemSettings]
net.Iris.UseIrisReplication=1
net.IsPushModelEnabled=1

[/Script/IrisCore.ObjectReplicationBridgeConfig]
DefaultSpatialFilterName=Spatial
; Pawns can be spatially filtered
+FilterConfigs=(ClassName=/Script/Engine.Pawn, DynamicFilterName=Spatial)
+FilterConfigs=(ClassName=/Script/EntityActor.SimObject, DynamicFilterName=None)

+DeltaCompressionConfigs=(ClassName=/Script/Engine.Pawn)
+DeltaCompressionConfigs=(ClassName=/Script/Engine.PlayerState)

[/Script/IrisCore.NetObjectGridFilterConfig]
ViewPosRelevancyFrameCount=600

这里对各个部分简单解释:

  • Engine:把 GameNetDriver 设置为可使用 Iris。
  • NetDriver:新增一个默认的 Channel 名为 DataStream,并设置静态 Channel Index 为 2(不要变,后面还用到!)。我们知道 0 号 Channel 是控制消息,1 号 Channel 是语音。Iris 本身不关心网络部分,只需要正确实现它的 DataStreamManager;目前 Epic 并没有给出一个全新优化后的网络底层,所以仍然使用了传统的网络通路,使用一个 Channel 传输所有的 Iris 数据。
  • SystemSettings:Iris 的开关。虽然用命令行参数(-UseIrisReplication)也可指定,不过这里建议使用配置文件的方式,后续对比测试来回切换也比较方便。如果以命令行参数启用,则一定要确保 client 和 server 的开启状况一致,否则自然是协议不同无法连接的。
  • ObjectReplicationBridgeConfig:官方给的一些 Iris 配置,包括官方已实现的 filter 和 delta 压缩等。这里主要是默认把对象路由到 spatial filter(类似于 Replication Graph 的 GridSpatialization2D)上。
  • NetObjectGridFilterConfig:这里是上面所说的 spatial filter 的配置。根据源代码的注释可知,这里的配置值用于指定“每多少帧必定复制一次”,是为了处理在 cell 边界时可能发生的裁剪问题。默认配置为 2(官方对自己这么没信心吗?),意思是即使已经超过了指定距离,仍然每 2 帧强制复制一次——这样的话测试结果根本就不对。因此先改为一个较大的值,等官方后续慢慢改好了去掉这个选项吧。

总的来看,即便是当前阶段,接入 Iris 也不算太麻烦,并且允许无痛地在 Iris 和 Legacy 之间切换(前提是没有魔改 rep graph)。说是“开箱即用”,问题也不大。

构造测试环境

模拟连接

之前官方在 NetcodeUnitTest 插件里提供了 UMinimalClient 机制,相信已经有很多人使用过。原理是模拟真实客户端连接到服务器,但是对于收到的 bunch 选择性处理(绝大多数都不做操作)。使用者可以继承拓展,实现对特定 bunch 或 RPC 等进行应答等。使用这样的机制,可以只开启一个客户端,但是创建上百个连接到服务器。

兼容 Iris

可惜的是,目前并不支持连接到基于 Iris 的服务器。实测发现需要进行如下改造。部分方式可能较为粗暴。

步骤一:将UMinimalClient::WriteControlLogin覆写。

void UIrisDemoMinimalClient::WriteControlLogin(FOutBunch* ControlChanBunch)
{
    uint8 MessageType = NMT_Login;
    FString BlankStr = TEXT("");
    FString OnlinePlatformName = TEXT("Dud");
    uint8 EncType = 0;

    FString ConnectURL = DefaultClientConnectURL + URLOptions;

    UNIT_LOG(, TEXT("Sending NMT_Login with parameters: ClientResponse: %s, ConnectURL: %s, UID: %s, OnlinePlatformName: %s"), *BlankStr, *ConnectURL, *JoinUID, *OnlinePlatformName);

    *ControlChanBunch << MessageType;
    *ControlChanBunch << BlankStr;
    *ControlChanBunch << ConnectURL;
    *ControlChanBunch << EncType;
    *ControlChanBunch << JoinUID;
    *ControlChanBunch << OnlinePlatformName;
}

可能需要对生成 ConnectURL 的部分进行自己的修改。修改的原因是,基类的对应函数,生成该 URL 时使用了 UUnitTest::UnitEnv,而该 static 变量是个空指针。

步骤二:屏蔽 UUnitTestManager::Serialize。即整个函数全部注释或进去就 return。原因是此处会 crash 且堆栈较为诡异,考虑到不是关键路径,暂时没有深究。有大师更加了解的也欢迎讨论。

步骤三:修改 UMinimalClient::SendInitialJoin

if (UNetDriver* NetDriver = UnitNetDriver.Get())
{
    // LocalNetworkFeatures = NetDriver->GetNetworkRuntimeFeatures();
    LocalNetworkFeatures = EEngineNetworkRuntimeFeatures::None;
    if (UE::Net::ShouldUseIrisReplication())
    {
        LocalNetworkFeatures |= EEngineNetworkRuntimeFeatures::IrisEnabled;
    }
}

Minimal Client 完全没有考虑过 Iris 的问题,所以此处也是写死 network features,导致连接时因 feature 不可用失败。这里在开启 Iris 时将对应 feature 标记上去。

步骤四:修改 UUnitTestChannel::Init

void UUnitTestChannel::Init(UNetConnection* InConnection, int32 InChIndex, EChannelCreateFlags CreateFlags)
{
    // If the channel type is still default, assume this is a control channel (since that is the only time ChType should be default)
    if (ChName == NAME_None)
    {
        // if (InChIndex != 0)
        // {
        //  UE_LOG(LogUnitTest, Warning, TEXT("Unit test channel type was NAME_None, for non-control channel"));
        // }
        //
        // ChName = NAME_Control;
        switch (InChIndex)
        {
        case 2:
            ChName = FName(TEXT("DataStream"));
            break;
        default:
            ChName = NAME_Control;
            break;
        }
        UE_LOG(LogUnitTest, Warning,
             TEXT("InChIndex=%d, channel type was NAME_None, renamed to %s"), InChIndex, *ChName.ToString());
    }

    Super::Init(InConnection, InChIndex, CreateFlags);
}

一段对于 Channel 的默认改名逻辑。之前说过 0 号通道是控制通道,而我们在配置文件里把 2 号通道分配给了 Iris 的 DataStream。这里加上对应逻辑,否则进入该函数后 2 号通道也被改名为 Control,导致 channel close。

步骤五:修改 UMinimalClient::ConnectMinimalClient

UnitNetDriver->ChannelDefinitionMap[FName(TEXT("DataStream"))].ChannelClass = UUnitTestChannel::StaticClass();

仍然是针对 DataStream 的问题。为了使数据包不被客户端逻辑所处理,对应的 channel 都要改为 UUnitTestChannel,来拦截其中逻辑。把 DataStream 也改过去。

创建假客户端

完成上述修改后,应该能够正常跑起来了。要做的事情就是创建大量的假玩家。方法可以参考 UClientUnitTest::ConnectMinimalClient

模拟业务场景

属性复制工作负载

为了测试方便,这里新建一个 actor component 专门模拟属性复制。之后需要测试的 actor(如 character)挂上这个组件就行。注意要按 Push Model 的规范写——一是目前的 Iris 依赖 Push Model 来标记哪些 object 发生了变化,二是只有 full push model 情况下的 Legacy 和 Iris 相比才比较公平。

角色分布

还是用普通的方式:GameMode 上覆写 FindPlayerStart_Implementation。我这里是允许在蓝图里指定中心点和正方形的分布范围。

非角色 Actor

模拟的是可交互的物品等,如“吃鸡”类游戏里的可拾取物。这里基于 AActor 派生一个类,挂静态网格体和前面说过的模拟属性复制的组件。

测试环境

运行平台:

  • CPU: AMD Ryzen 5975WX 32C64T @ 3.6 GHz
  • RAM: 64 GB × 2 @ 3200 MHz
  • GPU: NVIDIA GeForce RTX 4080
  • OS: Windows 11

Unreal 的构建目标为 Development Editor。测试启动 1 个 DS,Client 和 DS 均在本机运行;对且仅对 DS 开启性能 trace,后续使用 Unreal Insights 分析。

测试和分析

对一些典型情况对比测试。对 Legacy 开启 full Push Model 和 Replication Graph,且 Rep Graph 关闭 trick(dynamic node 底层不使用 freq bucket);Iris 的 Spatial filter 的强制复制间隔设置为 600 帧。

每组均是连接 100 个客户端。

组一:纯角色,密集

分布范围是边长 200 米的正方形。

(avg) Legacy Legacy + RG Iris
GameEngine Tick 64.79 ms 68.65 ms 14.07 ms
NetDriver TickFlush 58.05 ms 62.70 ms 8.68 ms
Per Conn Ops 450 μs 602 μs 50 μs

注:Per Conn Ops 指的是对于单个连接的集中操作。Legacy:Process Prioritized Actors Time;Legacy + RG:Process Gathered Lists;Iris:NetConnection Tick。对于单连接的操作还有很多,此处统计数据仅来源如上。后续也如此。

由于角色的 cull distance 是 250 米,所有角色之间都需要互相复制。因此 rep graph 对于视野相关性的优化在此处不会起到帮助,反而有额外开销(猜测 grid 维护以及排序打了大量 trace 影响较大),成绩劣于原始版本。

总体 Legacy 和 Iris 相比,也是劣势极为明显。毕竟 Iris 花大功夫在各种数据的复用上,优化效果最好的场景就是这种极端聚集。下面仔细看看:

200m Legacy 总览
这种密集场景下,能看到我们熟悉的耗时结构:90%的时间花费在网络复制上。业务场景越复杂,越难以优化,且给到中间业务逻辑 tick 的时间就更少。没有任何业务逻辑的 demo 在百人同屏的情况下只能跑到 13 fps 左右。
200m Legacy 单连接
而单个连接的处理,又能够看到基于 actor 粒度的复制,其中的每个 object 走“比较—序列化—发送”流程。
200m Legacy 单连接复制
进一步放大,可以看到 Legacy 的这个步骤确实耗时比较大,复制一个角色就要 4.5 μs,怪不得平均的每连接操作耗时差距有 9 倍。
200m Iris 总览
而 Iris 这边看起来就“健康”得多,因网络复制本身耗时大大降低,在一帧里的占比也没有那么夸张了。
200m Iris 网络tick
而网络操作部分,前期准备工作(pre send update)和对于每个连接的操作可以说是对半开。虽然前期准备工作耗时比 Legacy 高得多,但正是有了这些操作生产出的可复用信息,才能使 Iris 整体性能得到提升。复制同样多的信息,Iris 这边到 connection 时只需要依据精确到 property 的标记(ChangeMask)打序列化并发送出去即可。

组二:纯角色,稀疏

分布范围是边长 8000 米的正方形,这也是 PUBG 地图的大小。此时均匀分布的角色互相之间都超出裁剪距离,只有自己还需要复制。

(avg) Legacy Legacy + RG Iris
GameEngine Tick 11.79 ms 13.66 ms 11.91 ms
NetDriver TickFlush 7.16 ms 8.81 ms 6.70 ms
Per Conn Ops 48.3 μs 64.0 μs 24.4 μs

这次因为没有重复复制发送的数据,少了“无用功”,二者差别就不是那么大了。

8km RepGraph 排序trace
Replication Graph 开启时,每个连接下面的 actor 排序都会进行事无巨细的 trace 记录,怀疑因为这个问题导致结果里性能差于不开 Rep Graph(确实 per conn 差异悬殊)。
8km Iris Net Tick
看到 Iris 这边。Pre Send Update 阶段和之前的密集情况耗时基本相同;单连接对应耗时有所降低。基于此可知 Pre Send 阶段是固定开销,不随场景而变。

现在这个只有角色的稀疏场景,没有拉开差距,Iris 以微弱优势胜出。但 Iris 对比 Legacy,有没有什么情况会变得更差呢?可能需要再来看看稀疏场景下,除角色外还有别的 actor 的情况。

组三:角色与少道具,稀疏

分布范围是边长 8000 米的正方形。每个角色周围生成10个宝箱(即前文里挂载了属性复制测试组件的静态网格体)。因 actor 数量显著增多,从本组开始 Legacy 不使用非 Replication Graph 的版本。

(avg) Legacy + RG Iris
GameEngine Tick 26.26 ms 21.97 ms
NetDriver TickFlush 20.63 ms 12.84 ms
Per Conn Ops 179.0 μs 85.9 μs

Iris 以小幅优势胜出。这个场景下,Pre Send 耗时已经大于实际 send,不过二者还是接近一半一半。

组四:角色与多道具,稀疏

分布范围是边长 8000 米的正方形。每个角色周围生成20个宝箱。

(avg) Legacy + RG Iris
GameEngine Tick 38.13 ms 32.01 ms
NetDriver TickFlush 32.12 ms 24.28 ms
Per Conn Ops 288.0 μs 95.1 μs

这一组的分析,需要和上一组对比,看 actor 数量更多时,趋势是怎样的。可以看到 Legacy 的单连接复制耗时多出了 60%,而 Iris 只多出 11%——这说明即使是不可复用的部分,Legacy 的 “对比-序列化-发送” 流程也是太慢了。所以 Iris 流程中虽然存在一个固定开销 Pre Send,但整体是全新架构,整体来说仍是不落下风。

总结

为直观感受 UE 5.1 引入的实验特性 Iris Replication 的实际效果,我们使用一个全新实验工程,完成了 Iris 的引入、单机测试环境的改造和开发,并针对几种典型场景开展了对比测试。

从性能测试结果来看,Iris 在高度复用的密集场景下效果确实非常好,耗时几乎是仅占 Legacy 10%;而复用度较低的情况下,也没有发生负优化,大概可以在各种场景下使用。Legacy 也彻底体现出了 Connection-Actor 粗粒度下 Compare-Serialize-Send 模型的缺点,积重难返,这就不难知道为何 Iris 不再像 Rep Graph 或 Push Model 那样给引擎打补丁,而是选择重新开发一套真正高内聚低耦合的全新的复制系统。

然而,当前 UE 5.3 版本的 Iris 作为实验特性,还有几个大问题:

  • 官方没有给出全新的网络底层实现(问题不大,本来这一块自行优化才是更好的方案)
  • 虽然已经比较全面地考虑了各个引擎特性的协作,但官方没有做好全面兼容,比如上面提到的 NetcodeUnitTest
  • 代码里有大量的 TODO,不完善,摇摇欲坠
  • 还没有在大型上线项目(堡垒之夜?)得到过稳定性验证,官方也不认可用户现在就在生产环境使用

因此说全面替代还为时尚早,UE 6 才从 Experimental 里移出来都有可能。Legacy 和 Iris 二者并行是目光所及内最可能出现的情况。不妨再等等 Epic 的改进,在这期间也可以积极地尝试集成(如果是刚启动的新项目)。毕竟从实际情况来看 Iris 新架构带来的提升是很诱人的。

Archives Tip
QR Code for this page
Tipping QR Code