Unreal Engine 5 渲染相关的关键基建概述

内容纲要

Hineven(2025):这坑不想填了。。。

前言

笔者刚开始接触UE,博客中的大量陈述,若没有引用顶标,则可以认为是笔者基于自己对于UE代码的理解做出,夹带错误的可能是难以避免的,希望大家能包涵,也更希望各位能指正而共同进步。

笔者会选择性忽略一些代码中的内容,其中包括用于收集统计信息和调试信息,以及繁杂的平台相关的特殊处理,和较为不值一提的逻辑。

博客中会有一些1,此部分代码笔者目前无法理解并暂时被跳过,不过会在日后补全全。

文中的块状引用代表笔 者在阅读代码时的思考或杂谈,它们可能与主要内容关联不大,但如果到此 ,读者与笔者有相同或相似的思索乃至疑惑,这些内容可能会成为不错的点心。

标明源代码路径时,笔者默认折叠了PrivatePublic文件夹,如无特殊情况,省略了.cpp.h后缀。

  • 关于UE版本:

    笔者撰写本博客时,阅读的是UE 5.3的代码。UE的代码还在持续扩充与更新之中,更前或之后的版本内代码逻辑可能会与本文有任意出入,这是值得注意的。

  • 关于阅读代码所用的IDE:

    笔者尝试过CLion,但其在如此巨大工程上的各种操作响应实在太慢,而且左侧的Directory栏无法正确显示和折叠每个模组文件夹(你会看见一堆PrivatePublic文件夹作为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的核心模块被称为CoreEngine/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中,有大量类只被允许拥有一个公共或私有的实例,比如FShaderLibrariesCollectionFTaskGraph的实现实例等,这唯一实例称为单例,这种实现方式称为单例模式(Singleton)。

一部分单例被存储在static FSomeClass* Impl之中,而Impl大概会被声明为FSomeClasspublic成员,调用者们可以直接访问。另一部分单例可以通过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()

  1. 初始化日志,设置当前线程为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();
      }
  2. 从环境变量增添命令行参数。

  3. 设置游戏名,同时设置工程或游戏文件的根目录。

      {
          SCOPED_BOOT_TIMING("LaunchSetGameName");
    
          // Set GameName, based on the command line
          if (LaunchSetGameName(CmdLine, GameProjectFilePathUnnormalized) == false)
          {
              // If it failed, do not continue
              return 1;
          }
      }
  4. 初始化和启动调试相关功能,这其中包括LLM(Low Level Memory Tracker)相关功能。

    • 关于LLM:与它相关的宏在引擎逻辑里随处可见,且其缩写对我们这种初学者带有“这玩意很重要!”的迷惑性质(LLM, Low Level Memory),但实际上并不需要关注它。因为它的功能纯粹服务于调试,在Shipping版本中根本不会被编译。

    • 后文中,笔者将省略大部分只和调试与日志相关的逻辑。

  5. 在Delegate上注册几个比较基本的回调,比如物理系统垃圾回收、输入事件触发时间戳更新等。

  6. 从工程中加载PreInit阶段应当被加载的模块,比如解密打包工程文件的模块。此模块固定从ProjectDir/Binaries/[PlatformName]/[ExecutableName]-[ProjectName]PreInit.[dllext]加载,以后UE5开发的游戏加密模块代码大概都放这里。此模块被加载后,其DLL Handle不会被引擎被保存。

  7. 如果不是Shipping版本,则从ProjectDir/Config/CommandLineAliases加载额外ini文件,以其中数据展开命令行参数。

  8. 初始化IO,相关代码在Core/IO下,其中FIODispatcherImpl也继承自FRunnable,IO任务似乎依然是生产者消费者模型。这部分代码好像会将大批量IO请求Baching以提升速度,笔者没有细看3

  9. 注册LocalizationManager的回调,它会在.pak打包文件加载后开始初始化(从中并行地解包翻译数据)。

  10. 调用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();
    // ...
    };
  11. 重载文件。

    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;
      }
    }
  12. 文件系统初始化,然后令IFileManager单例处理命令行参数。

    • IFileManager目前在UE中的唯一实现是FFileManager,其每次从FPlatformFileManager单例处获得当前接入的平台文件系统(实现了IPlatformFile接口的类的实例),并包装了其方法。

    从抽象层级上讲,IFileManagerIPlatformFile的上层,而IPlatformFIle在本地文件系统、网络等可以提供文件系统功能的基础设施的上层。

  13. 调用FPlatformFileManager::InitializeNewAsyncIO()1

  14. 广播FileSystelReady的Delegate,其它模组可能依赖于此Delegate来加载一些数据文件。

  15. 标记本线程为游戏线程,并设置对应全局变量。

  16. 读取更多命令行参数,并决定引擎运行模式

    UE有三种运行模式:

    • RegularClient:客户端模式。

    • DedicatedServer:服务端模式,此状态下,UE不会有显示前端,而专注于游戏逻辑。

    • Commandlet:命令行工具模式,此模式的引擎用于提供引擎执行环境给开发者编写的自动化工具使用,比如打包游戏资源的Cooking就运行在此模式下。

    UE会综合多方面情况(编译宏、命令行参数等)来判断引擎当前应该以哪个模式启动,并处理对应的命令行参数,设置对应的全局标签。

  17. 判断是否启用确定性执行模式。

    确定性模式是一个专门用于复现情景的模式,在此模式下,引擎时间片大小是固定的,整个引擎的初始随机种子可以指定为特定的数字。

  18. 通过某些规则修正项目目录,确认项目文件路径,尝试读取项目文件。

    在这一步,即便是可执行版引擎(不为UE编辑器),若能,也会尝试读取项目文件。如果读取失败,引擎会直接退出。

  19. 增添引擎预设Shader目录,如果与编辑器一起运行,尝试创建Shader使用的暂存文件夹。这些Shader可能实际上存储在.pak打包文件内,不过通过IPlatformFIle虚拟了路径。

  20. 创建TaskGraph和两到三个不同优先级的FQueuedThreadPool,并创建其Worker Threads。

    值得注意的是,在带有Editor的UE中,Engine使用的ThreadPool是Editor的ThreadPool的一个虚拟化,UE通过FThreadPoolWrapper制约了Engine所能使用的线程数量。

    很神奇的一点是,TaskGraph和FQueuedThreadPool每个都会创建大约并行数-2个线程出来,这个数量一般大于系统真正的并行数。UE在注释中给出的解答是,通过准确设置线程优先级避免了不同线程间的干扰。TaskGraph实现中的高性能线程优先级为SlightlyBelowNormal,因此所有FQueuedThreadPool线程优先级最高被设置为BelowNormal。而最低优先级的GBackgroundPriorityThreadPool拥有Lowest优先级。

  21. 广播TaskGraphSystemReady事件,一部分依赖多任务系统的初始化可以开始了。

  22. 开始加载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,可以接受执行任何命令。

    提一笔这个FExecFMallocUPlayerUEngineFVulkanCommandsHelper等一系列类型都直接或间接地继承了此类,并把调试和执行命令的功能写在了里面。当一个命令从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宏定义,实际上是模组类的工厂函数。
    • 接下来就和加载静态链接的模组一致了。

    (排除更早的、可能存在的预加载模组)引擎启动到现在为止的第一个模组加载如此完成。

  23. 处理更多命令行参数,由于这是引擎运行至今第一次使用IConsoleManager,懒加载其单例。

    FConsoleManager在此阶段被初始化与创建,一系列基本的CVar被注册,来自于命令行和配置文件的CVar重载被应用。

    CVar是Console Variable的简称8

  24. 调用Oodle的PreInit(),这似乎是UE使用的压缩引擎但不由Epic开发9,也不是开源软件,不知道它两怎么授权的UE包装了其SDK。略过。

  25. 加载更多模组(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的初始化逻辑会在后文中进行介绍。

  26. 调用AppInit()

    • 初始化文本本地化系统,略过。

    • 平台相关初始化,此时调用FPlatformMiscFPlatformApplicationMisc,这二者皆通过Typedef作为FWindowsPlatformMiscFWindowsPlatformApplicationMisc的假名,UE通过这种方式在编译时适配不同平台。

    • 工程和平台、内存分配相关初始化与命令行参数处理,比较繁杂,忽略。

    • 加载IoStoreOnDemand模组,这是文件系统后端,笔者暂时还不是很清楚1

    • ini配置系统初始化完毕,广播相关Delegate。

    • 加载工程与全局启用的、加载时间声明为EarliestPossible的所有插件,这也是一般的用户插件最早可以被加载的位置,完毕后广播相关Delegate。

    • 检查项目插件是否过期或遗失。过期的插件会被重新编译。当然这只在编辑器存在且能被找到时执行。

    • FEmbeddedCommunication::ForceTick(16);:不是很懂1

    • 加载PostConfigInit的工程和全局启用的用户插件。

    • FEmbeddedCommunication::ForceTick(17);,这里出现了大量类似代码,似乎和操作系统与程序信号有关,限于笔者水平还无法理解1

    • 早初始化头戴设备,忽略。

暂时不深入,先看EngineInit()EngineInit()调用了GEngineLoop.Init(),包含以下逻辑:

  • 按配置(是否启用Editor)选取实现了UEngine的模块加载(选择GameEngineUnrealEdEngine),创建GEngine,这是引擎实例本体。

  • 播放一些时间刻,这或许用于在

上文中提到,UE的渲染逻辑执行在ENamedThreads::ActualRenderingThread上,运行着RenderCore/RenderingThread内的逻辑。

  • FRenderingThread

    • 继承了FRunnable

    • Init():将GRenderThreadID设为自身线程ID,用于随处访问10

    • Run():渲染主逻辑入口,调用RenderingThreadMain()

  • RenderingThreadMain()

    • 传入一个标识TaskGraph已经绑定到渲染线程的FEvent

    • FEventCore/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

TShaderRefShader的引用·。

TShaderMapRef:存储于ShaderMapShader的引用,但无法引用GlobalShaderMap中的Shader

GlobalShaderMap:全局ShaderMap。

渲染流程

参考资料


  1. 留个坑。 

  2. UE的模块也可以静态链接。 

  3. 留个坑,以后或许会更新UE IO相关的博客,讲一下这个比较迷你的子模块。 

  4. 但又无法忽略,笔者计划在后续的博客中加入这一部分。 

  5. Core/HAL和Platform的一部分,或许在日后会详细介绍。 

  6. 这似乎又是一个大话题,与UObject(一个庞大而复杂的系统)、编译和Verse这种新特性有一定关系,笔者以后可能会加以补充或单独撰写另一篇博客。 

  7. 他已经忘了此约定的出处。 

  8. Console Variables in C++ | Unreal Engine Documentation 

  9. Oodle Compression - radgametools.com 

  10. UE在不久的将来将会抛弃这个全局变量。 

此条目发表在学习分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注