Hineven(2025):这坑不想填了。。。
前言
笔者刚开始接触UE,博客中的大量陈述,若没有引用顶标,则可以认为是笔者基于自己对于UE代码的理解做出,夹带错误的可能是难以避免的,希望大家能包涵,也更希望各位能指正而共同进步。
笔者会选择性忽略一些代码中的内容,其中包括用于收集统计信息和调试信息,以及繁杂的平台相关的特殊处理,和较为不值一提的逻辑。
博客中会有一些1,此部分代码笔者目前无法理解并暂时被跳过,不过会在日后补全全。
文中的块状引用代表笔 者在阅读代码时的思考或杂谈,它们可能与主要内容关联不大,但如果到此 ,读者与笔者有相同或相似的思索乃至疑惑,这些内容可能会成为不错的点心。
标明源代码路径时,笔者默认折叠了Private
和Public
文件夹,如无特殊情况,省略了.cpp
和.h
后缀。
-
关于UE版本:
笔者撰写本博客时,阅读的是UE 5.3的代码。UE的代码还在持续扩充与更新之中,更前或之后的版本内代码逻辑可能会与本文有任意出入,这是值得注意的。
-
关于阅读代码所用的IDE:
笔者尝试过CLion,但其在如此巨大工程上的各种操作响应实在太慢,而且左侧的Directory栏无法正确显示和折叠每个模组文件夹(你会看见一堆
Private
和Public
文件夹作为CMake的虚假目标的源码文件夹堆叠在Directory栏里,而根本不知道它们属于什么模块!)。笔者也尝试过VS Studio(这也是官方推荐的IDE,给了详细的配置过程),但是不大喜欢,快捷键也用得不熟练。最终笔者选择了Rider for Unreal。这个Rider似乎对编辑Unreal代码做了特殊优化所以导览代码的响应速度很快,快捷键和界面也符合笔者的使用习惯,目前也没有什么不好的地方。如果你以前是JetBrain产品的使用者,那么这个IDE一定值得尝试。
UE的编译过程
Youtube上有一个很不错的视频,它让笔者更好的理解了UE的编译流程。
UE使用模块化设计控制复杂度,UE被分为许多模块,每个模块有一个CSharp源码文件(xxx.build.cs
)描述其依赖模块、编译参数等各种要求。Unreal的编译流水线大致如下:
-
编译脚本调用UBT(Unreal Build Tool,一个使用CSharp开发的编译辅助工具)扫描并读取所有模块的编译要求。
-
UBT调用UHT(Unreal Header Tool)对所有代码的头文件进行预处理,按照原始C++代码中的空宏生成Reflection所需的代码和新的头文件,被命名为
x.generated.h
,并会被cpp
文件自动导入 -
UBT接着会生成运行平台所拥有的C++编译工具链所需的文件(比如Makefile,在Windows下就是VS Studio的
sln
文件,可以调用MSVC)。约定俗称地,每个模组的代码文件被分为Public和Private两个文件夹中,在xxx.build.cs
中被当前模组依赖的模组的Public
代码将被暴露给当前模组(比如头文件等)并被使用。 -
最终,大部分模块2被编译为了一个个DLL,在运行时动态加载。
UE的核心模块被称为Core
(Engine/Source/Core
),其包含UE在各种语境下运行(普通模式,服务端模式或作为命令行工具启动)所需的核心内容。Core
的源代码下有Modules
子文件夹,其中有着模组的探查、解包、热加载和卸载相关的代码。
UE命名和代码规范
-
类名称前缀
类名称默认为
前缀字母+CamelCase
。-
模板类以
T
开头,比如TRefCountPtr
。 -
继承
UObject
且不为AActor
子类的类需以U
开头。 -
继承
AActor
的类需以A
开头。 -
继承
SWidget
的类以S
开头。 -
接口类(包含大量纯虚函数的类)以
I
开头。 -
Concept以
C
开头。 -
枚举类以
E
开头,成员以EXXXXX_
开头,其中XXXXX
是类名缩写。
这些名称可能影响到UHT对于源代码的理解,因此必须遵守。
-
-
成员与函数参数名称规范
成员与函数参数名都为
CamelCase
,有时需要前缀,特殊情况下可以使用下划线。-
布尔成员变量以
b
开头。 -
使用下划线的例子是:
FlushPendingDeleteRHIResources_RenderThread()
和FlushPendingDeleteRHIResources_GameThread()
,表示其中一个函数执行在RenderThread,另一个在GameThread上。
-
-
函数参数命名规范
-
推荐使用
In
前缀那些作为输入参数的函数参数。 -
推荐使用
Out
前缀那些可能成为输出的函数参数。
-
-
全局变量命名规范:使用
G
作为前缀。 -
宏命名规范:全部使用
UPPER_SNAKE_CASE
。 -
其它规范比如不准使用auto等影响不大,因此随意。
UE之单例
在UE中,有大量类只被允许拥有一个公共或私有的实例,比如FShaderLibrariesCollection
,FTaskGraph
的实现实例等,这唯一实例称为单例,这种实现方式称为单例模式(Singleton)。
一部分单例被存储在static FSomeClass* Impl
之中,而Impl
大概会被声明为FSomeClass
的public
成员,调用者们可以直接访问。另一部分单例可以通过FSomeClass::Get()
方法获得其指针。
笔者第一次阅读到FAnyClass::Impl时,误认为这是指向实现FAnyClass接口的子类实例,但实际上许多情况下似乎并非如此,这依然是单例的另一种展现方式。
UE的多线程和调度
这部分无可忽视,但是一个比较大的话题,见另一篇博客。
从启动到进入渲染逻辑
UE的入口函数在Launch
模块下,Launch
模块下有多个子文件夹,用于在不同的平台上编译。在Windows下,Launch/Windows/LaunchWindows.cpp
中的int32 WINAPI WinMain(_In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char* pCmdLine, _In_ int32 nCmdShow)
被导出并链接为可执行,这也是整个UE的入口函数。
int32 WINAPI WinMain(_In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char* pCmdLine, _In_ int32 nCmdShow)
{
int32 Result = LaunchWindowsStartup(hInInstance, hPrevInstance, pCmdLine, nCmdShow, nullptr);
LaunchWindowsShutdown();
return Result;
}
在LaunchWindowsStartup()
方法中,顺序执行了以下几个任务:
-
Parse命令行参数,设置一系列全局变量来决定运行模式、调试等级等。
-
执行
GuardedMain()
。 -
退出后如果在调试模式下,尝试依赖平台处理
errno
和抛出的异常。
GuardedMain()
则是所有平台通用的主函数,在Launch/Launch.cpp
中,其逻辑大致包括。
-
若有,等待调试器挂载。
-
广播启动事件,响应者会被马上执行。
-
创建Cleanup Guard。
-
调用
EnginePreInit()
。 -
调用
EditorInit()
(如果有)和EngineInit()
。 -
启动TickLoop。
- 和所有游戏引擎一样,无论是最原始的、玩具级的,还是庞大而复杂如UE一般的引擎,都逃不开最朴素的逻辑。
while( !IsEngineExitRequested() ) { EngineTick(); }
-
若有编辑器,清理之。
先看EnginePreInit()
,它调用了GEngineLoop.PreInit()
,在Launch/LaunchEngineLoop
中:
-
调用
PreInitPreStartupScreen()
:这就是UE的启动屏幕了。 -
如果失败,清除上下文并退出。
-
调用
PreInitPostStartupScreen()
。 -
同样的,如果失败,清除上下文并退出。
深入其中,忽略平台相关逻辑,看PreInitPreStartupScreen()
:
-
初始化日志,设置当前线程为Logging线程,这会调用
GLog
(实际上是一个宏,定位到GetGlobalLogSingleton
)的方法,而其是一个FOutputDeviceRedirector
继承自FOutputDevice
用于抽象日志输出设备。// GLog is initialized lazily and its default primary thread is the thread that initialized it. // This lazy initialization can happen during initialization of a DLL, which Windows does on a // worker thread, which makes that worker thread the primary thread. Make this the primary thread // until initialization is far enough along to try to start a dedicated primary thread. if (GLog) { GLog->SetCurrentThreadAsPrimaryThread(); }
-
从环境变量增添命令行参数。
-
设置游戏名,同时设置工程或游戏文件的根目录。
{ SCOPED_BOOT_TIMING("LaunchSetGameName"); // Set GameName, based on the command line if (LaunchSetGameName(CmdLine, GameProjectFilePathUnnormalized) == false) { // If it failed, do not continue return 1; } }
-
初始化和启动调试相关功能,这其中包括LLM(Low Level Memory Tracker)相关功能。
-
关于LLM:与它相关的宏在引擎逻辑里随处可见,且其缩写对我们这种初学者带有“这玩意很重要!”的迷惑性质(LLM, Low Level Memory),但实际上并不需要关注它。因为它的功能纯粹服务于调试,在Shipping版本中根本不会被编译。
-
后文中,笔者将省略大部分只和调试与日志相关的逻辑。
-
-
在Delegate上注册几个比较基本的回调,比如物理系统垃圾回收、输入事件触发时间戳更新等。
-
从工程中加载
PreInit
阶段应当被加载的模块,比如解密打包工程文件的模块。此模块固定从ProjectDir/Binaries/[PlatformName]/[ExecutableName]-[ProjectName]PreInit.[dllext]
加载,以后UE5开发的游戏加密模块代码大概都放这里。此模块被加载后,其DLL Handle不会被引擎被保存。 -
如果不是Shipping版本,则从
ProjectDir/Config/CommandLineAliases
加载额外ini
文件,以其中数据展开命令行参数。 -
初始化IO,相关代码在
Core/IO
下,其中FIODispatcherImpl
也继承自FRunnable
,IO任务似乎依然是生产者消费者模型。这部分代码好像会将大批量IO请求Baching以提升速度,笔者没有细看3。 -
注册LocalizationManager的回调,它会在
.pak
打包文件加载后开始初始化(从中并行地解包翻译数据)。 -
调用
FShaderCodeLibrary::PreInit()
,实际上其只是注册了.pak
打包文件挂载后的回调,同样的,在打包文件被加载后开始载入工程的Shader。-
工程本地的Shader加载过程此处不在深入4。
-
这其中比较有意思的一点是,
.pak
文件挂载完毕的Delegate叫做OnPakFileMounted2
。这个2
我还不知道是什么东西,四处找了找,也没有找到OnPakFileMounted1
啊!意义不明。
除此之外还笔者还看到一些迷惑代码比如调用了
const
修饰的函数、获取了返回值但什么操作都没有干之类。或许即便是UE,代码质量也不能做到极致吧。class FCoreDelegates { public: // After a pakfile is mounted this is called UE_DEPRECATED(5.3, "This delegate is not thread-safe, please use GetOnPakFileMounted2().") static CORE_API TMulticastDelegate
OnPakFileMounted2; static CORE_API TTSMulticastDelegate & GetOnPakFileMounted2(); // ... }; -
-
重载文件。
UE假定项目文件被分为多类(日志,内容,等等),每类文件可以存储在不同平台上,使用
IPlatformFile
抽象某一平台上的的文件系统5,当前使用的默认文件系统平台可以从FPlatformFileManager
单例处获得。图为
IPlatformFile
的所有子类,它们的实现分散在各个模块之中,此处,UE允许一部分PlatformFile按照命令行指定替换(重载)为其它平台上的文件系统。IPlatformFile
直接支持将文件系统串成一串层层包装层层扩展的操作,可以通过GetLowLevel()
方法获得上层IPlatformFile*
。UE文件组织比笔者想像的更加复杂,可能会在后续博客中补充1。// allow the command line to override the platform file singleton bool bFileOverrideFound = false; { SCOPED_BOOT_TIMING("LaunchCheckForFileOverride"); if (LaunchCheckForFileOverride(CmdLine, bFileOverrideFound) == false) { // if it failed, we cannot continue return 1; } }
-
文件系统初始化,然后令
IFileManager
单例处理命令行参数。IFileManager
目前在UE中的唯一实现是FFileManager
,其每次从FPlatformFileManager
单例处获得当前接入的平台文件系统(实现了IPlatformFile
接口的类的实例),并包装了其方法。
从抽象层级上讲,
IFileManager
在IPlatformFile
的上层,而IPlatformFIle
在本地文件系统、网络等可以提供文件系统功能的基础设施的上层。 -
调用
FPlatformFileManager::InitializeNewAsyncIO()
1。 -
广播FileSystelReady的Delegate,其它模组可能依赖于此Delegate来加载一些数据文件。
-
标记本线程为游戏线程,并设置对应全局变量。
-
读取更多命令行参数,并决定引擎运行模式
UE有三种运行模式:
-
RegularClient:客户端模式。
-
DedicatedServer:服务端模式,此状态下,UE不会有显示前端,而专注于游戏逻辑。
-
Commandlet:命令行工具模式,此模式的引擎用于提供引擎执行环境给开发者编写的自动化工具使用,比如打包游戏资源的Cooking就运行在此模式下。
UE会综合多方面情况(编译宏、命令行参数等)来判断引擎当前应该以哪个模式启动,并处理对应的命令行参数,设置对应的全局标签。
-
-
判断是否启用确定性执行模式。
确定性模式是一个专门用于复现情景的模式,在此模式下,引擎时间片大小是固定的,整个引擎的初始随机种子可以指定为特定的数字。
-
通过某些规则修正项目目录,确认项目文件路径,尝试读取项目文件。
在这一步,即便是可执行版引擎(不为UE编辑器),若能,也会尝试读取项目文件。如果读取失败,引擎会直接退出。
-
增添引擎预设Shader目录,如果与编辑器一起运行,尝试创建Shader使用的暂存文件夹。这些Shader可能实际上存储在
.pak
打包文件内,不过通过IPlatformFIle
虚拟了路径。 -
创建TaskGraph和两到三个不同优先级的
FQueuedThreadPool
,并创建其Worker Threads。值得注意的是,在带有Editor的UE中,Engine使用的ThreadPool是Editor的ThreadPool的一个虚拟化,UE通过
FThreadPoolWrapper
制约了Engine所能使用的线程数量。很神奇的一点是,TaskGraph和
FQueuedThreadPool
每个都会创建大约并行数-2个线程出来,这个数量一般大于系统真正的并行数。UE在注释中给出的解答是,通过准确设置线程优先级避免了不同线程间的干扰。TaskGraph实现中的高性能线程优先级为SlightlyBelowNormal,因此所有FQueuedThreadPool
线程优先级最高被设置为BelowNormal。而最低优先级的GBackgroundPriorityThreadPool
拥有Lowest优先级。 -
广播
TaskGraphSystemReady
事件,一部分依赖多任务系统的初始化可以开始了。 -
开始加载UE核心模块。
// Load Core modules required for everythin else to work (needs to be loaded before InitializeRenderingCVarsCaching) { SCOPED_BOOT_TIMING("LoadCoreModules"); if (!LoadCoreModules()) { UE_LOG(LogInit, Error, TEXT("Failed to load Core modules.")); return 1; } }
实际上,这个函数只会加载
CoreUObject
模块。除此之外,可以稍微探入加载模组的逻辑中阅读,但限于能力和时间笔者暂时无法细探6,此处仅做非常粗略的介绍。Core/Modules/ModuleManager
是UE的模组管理器,负责管理模组的索引与已经加载的模组、加载和卸载模组。它继承于FSelfRegisteringExec
又继承于FExec
,可以接受执行任何命令。提一笔这个
FExec
。FMalloc
、UPlayer
、UEngine
、FVulkanCommandsHelper
等一系列类型都直接或间接地继承了此类,并把调试和执行命令的功能写在了里面。当一个命令从UE Console发出的时候,除非命令被提前响应,所有继承了FSelfRegisteringExec
的类的实例都可以通过Exec虚函数接收到这条命令,而在UE Console输命令又很简单。因此继承
FSelfRegisteringExec
可能是穿透厚实的UE UI,实现调试功能的比较简便的方式。FModuleManager
会先从已加载模组里寻找模组,若无法找到,则进入模组加载逻辑。首先考虑加载静态链接的模组。这部分静态链接的模组的描述符、初始化函数地址、代码等都已经随着可执行文件被加载到了虚拟内存中,因此直接调用加载模组逻辑即可。
- 如果能,广播UObject加载Delegate(ProcessLoadedObjectsCallback),确保所有先于此模组的UObject都加载完毕。
此处,UE先推迟了字符串表的查询。字符串表一般用于本地化,推迟后的查询会在UObject完成加载后继续,此处笔者还不是很理解其中详细原因。
- 先广播静态链接过来的模组加载逻辑,这是一个Delegate,Delegate绑定的逻辑也由UE实现。
UE规定模组必须在源代码中插入的
IMPLEMENT_MODULE
宏用继承了IModuleInterface
的模组为参数定义了模板类FStaticallyLinkedModuleRegistrant
的静态实例,其构造函数会将用户的模组类构造方式注册到懒加载的注册表全局变量中。在静态链接时,此构造函数内的逻辑会被插入到_init()中,在程序的main函数前执行。Monolithic(传统)模式构建的UE是纯静态链接的。了解过类似方法的读者可能会发现一个问题:如果将外部模块编译为Library链接到可执行上,则会产生由于Library内注册用全局变量符号未被使用,链接器将整个符号优化掉导致构造函数根本不执行,注册失效的情况出现。按照JeremyGuo描述的一般约定7,链接器会剔除Library中未被链接器前方指令中代码使用的符号,而UE是怎么保证符号不被剔除的呢?
实际上在这种编译模式下,UE在编译中并没有把各个模组编译为静态链接库后与可执行文件链接,而是直接与可执行文件一遍链接所有源文件的obj文件。这也避免了未被可执行模块调用的全局成员变量在链接中被编译器”优化“而导致其构造函数无法运行的问题。
UE并没有什么魔法解决静态链接时这种利用全局成员变量构造函数往main函数前插入代码执行的方式失效的问题,而是采取了最简单粗暴的手段防止了此问题的发生。
-
再次广播
FModuleManager
内置的ProcessLoadedObjectsCallback Delegate,此Delegate被CoreUObject/UObject/Obj
监听并在此时加载新模组内的反射信息,不过现在CoreUObject
还未加载,因此无人监听。 -
调用
IModuleInterface::StartupModule()
,这也是模组自己实现的启动逻辑。 -
计算模组的加载位次,以获取模组的卸载顺序(后加载,先卸载)。
因为
StartupModule()
内可能会反过来调用FModuleManager
加载其依赖模组!- 广播模组加载Delegate。
如果静态链接模组中无法找到同名模组,则开始考虑动态链接的模组。
这只在非Monolithic模式下编译的UE有效。
- 广播ProcessLoadedObjectsCallback Delegate,确保所有先前模组UObject已经加载完毕。
- 在所有查找路径下寻找DLL,调用平台提供的DLL加载方法。
- 再次广播ProcessLoadedObjectsCallback Delegate,因为在DLL加载过程中,模组的
init
(由编译器生成的、进行静态成员初始化的函数)被执行,静态的UObject需要被优先注册处理。 - 抓取DLL中导出函数
InitializeModule
的指针,并执行,生成模组实例。此函数由每个模组都需要包含的DECLARE_MODULE
宏声明,IMPLEMENT_MODULE
宏定义,实际上是模组类的工厂函数。 - 接下来就和加载静态链接的模组一致了。
(排除更早的、可能存在的预加载模组)引擎启动到现在为止的第一个模组加载如此完成。
-
处理更多命令行参数,由于这是引擎运行至今第一次使用IConsoleManager,懒加载其单例。
FConsoleManager
在此阶段被初始化与创建,一系列基本的CVar被注册,来自于命令行和配置文件的CVar重载被应用。CVar是Console Variable的简称8。
-
调用Oodle的
PreInit()
,这似乎是UE使用的压缩引擎但不由Epic开发9,也不是开源软件,不知道它两怎么授权的UE包装了其SDK。略过。 -
加载更多模组(
LoadPreInitModules()
)完成UObject的加载和CVar导入后,UE开始加载更多核心模组,其中包括:
-
Engine
-
Renderer
-
AnimGraphRuntime
-
平台相关的
PreInit
模组。
UE通过
FGenericPlatformApplicationMisc
基类的派生类表示不同平台上应用所需执行的独特代码。其带有多个静态钩子函数,通过宏在编译时控制具体使用哪一个派生类作为FPlatformApplicationMisc
,来适配多个平台。-
SlateRHIRenderer(Slate UI是UE的UI库,这似乎是其渲染模组)
-
Landscape
-
RenderCore,渲染框架库。
奇怪的是,Renderer在RenderCore之前被加载。UE的Renderer本身应该依赖于RenderCore,这需要更多考察,笔者在完成RenderCore和Renderer的大致阅读后会补充相关内容。
-
TextureCompress
-
Virtualization,似乎主要用于在开发中虚拟化打包后的游戏运行环境。
-
AudioEditor
-
AnimationModifiers
Engine、Renderer和RenderCore的初始化逻辑会在后文中进行介绍。
-
-
调用
AppInit()
-
初始化文本本地化系统,略过。
-
平台相关初始化,此时调用
FPlatformMisc
和FPlatformApplicationMisc
,这二者皆通过Typedef作为FWindowsPlatformMisc
和FWindowsPlatformApplicationMisc
的假名,UE通过这种方式在编译时适配不同平台。 -
工程和平台、内存分配相关初始化与命令行参数处理,比较繁杂,忽略。
-
加载
IoStoreOnDemand
模组,这是文件系统后端,笔者暂时还不是很清楚1。 -
ini
配置系统初始化完毕,广播相关Delegate。 -
加载工程与全局启用的、加载时间声明为
EarliestPossible
的所有插件,这也是一般的用户插件最早可以被加载的位置,完毕后广播相关Delegate。 -
检查项目插件是否过期或遗失。过期的插件会被重新编译。当然这只在编辑器存在且能被找到时执行。
-
FEmbeddedCommunication::ForceTick(16);
:不是很懂1。 -
加载
PostConfigInit
的工程和全局启用的用户插件。 -
FEmbeddedCommunication::ForceTick(17);
,这里出现了大量类似代码,似乎和操作系统与程序信号有关,限于笔者水平还无法理解1。 -
早初始化头戴设备,忽略。
-
暂时不深入,先看EngineInit()
,EngineInit()
调用了GEngineLoop.Init()
,包含以下逻辑:
-
按配置(是否启用Editor)选取实现了
UEngine
的模块加载(选择GameEngine
或UnrealEdEngine
),创建GEngine
,这是引擎实例本体。 -
播放一些时间刻,这或许用于在
上文中提到,UE的渲染逻辑执行在ENamedThreads::ActualRenderingThread
上,运行着RenderCore/RenderingThread
内的逻辑。
-
FRenderingThread
-
继承了
FRunnable
。 -
Init()
:将GRenderThreadID
设为自身线程ID,用于随处访问10。 -
Run()
:渲染主逻辑入口,调用RenderingThreadMain()
。
-
-
RenderingThreadMain()
-
传入一个标识TaskGraph已经绑定到渲染线程的
FEvent
。 -
FEvent
是Core/HAL
下的平台无关的事件接口。用于封装平台提供的跨线程信号功能。 -
绑定到TaskGraph。
-
触发绑定完成事件,广播Delegate等其他逻辑。
-
获得
RHI
上下文(如果有,比如OpenGL)。 -
进入TaskGraph处理逻辑,直到请求中之。
-
广播Delegate,清理并退出。
/** The rendering thread main loop */ void RenderingThreadMain( FEvent* TaskGraphBoundSyncEvent ) { LLM_SCOPE(ELLMTag::RenderingThreadMemory); ENamedThreads::Type RenderThread = ENamedThreads::Type(ENamedThreads::ActualRenderingThread); ENamedThreads::SetRenderThread(RenderThread); ENamedThreads::SetRenderThread_Local(ENamedThreads::Type(ENamedThreads::ActualRenderingThread_Local)); FTaskGraphInterface::Get().AttachToThread(RenderThread); FPlatformMisc::MemoryBarrier(); // Inform main thread that the render thread has been attached to the taskgraph and is ready to receive tasks if( TaskGraphBoundSyncEvent != NULL ) { TaskGraphBoundSyncEvent->Trigger(); } // set the thread back to real time mode FPlatformProcess::SetRealTimeMode(); #if STATS if (FThreadStats::WillEverCollectData()) { FTaskTagScope Scope(ETaskTag::ERenderingThread); FThreadStats::ExplicitFlush(); // flush the stats and set update the scope so we don't flush again until a frame update, this helps prevent fragmentation } #endif FCoreDelegates::PostRenderingThreadCreated.Broadcast(); check(GIsThreadedRendering); { FTaskTagScope TaskTagScope(ETaskTag::ERenderingThread); struct FScopedRHIThreadOwnership { /** Tracks if we have acquired ownership */ bool bAcquiredThreadOwnership = false; FScopedRHIThreadOwnership() { // Acquire rendering context ownership on the current thread, unless using an RHI thread, which will be the real owner if (!IsRunningRHIInSeparateThread()) { bAcquiredThreadOwnership = true; RHIAcquireThreadOwnership(); } } ~FScopedRHIThreadOwnership() { // Release rendering context ownership on the current thread if we had acquired it if (bAcquiredThreadOwnership) { RHIReleaseThreadOwnership(); } } } ThreadOwnershipScope; FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(RenderThread); } FPlatformMisc::MemoryBarrier(); check(!GIsThreadedRendering); FCoreDelegates::PreRenderingThreadDestroyed.Broadcast(); #if STATS if (FThreadStats::WillEverCollectData()) { FThreadStats::ExplicitFlush(); // Another explicit flush to clean up the ScopeCount established above for any stats lingering since the last frame } #endif ENamedThreads::SetRenderThread(ENamedThreads::GameThread); ENamedThreads::SetRenderThread_Local(ENamedThreads::GameThread_Local); FPlatformMisc::MemoryBarrier(); }
-
场景结构和数据
线程间交互
材质系统
虚幻HAL
对硬件的抽象,内存模型,线程模型,原子操作等。
虚幻RHI
对渲染API的抽象。
Shader管理
ShaderMap
:存储一整套Shader,继承自FShaderMapBase
。
TShaderRef
:Shader
的引用·。
TShaderMapRef
:存储于ShaderMap
中Shader
的引用,但无法引用GlobalShaderMap
中的Shader
。
GlobalShaderMap
:全局ShaderMap。