文章

资源Cook构建

默认情况下,在 UE 中打包项目时,会拉起 BuildCookRun 来执行 Compile/Cook/Pak/Stage 等一系列流程。

在 UE 中,只有参与 Cook 的资源才会被打包

可以实现自定义的 Cook 过程,将 Cook 任务分配至不同的进程乃至机器实现并行化,加速 UE 的构建过程。

留意一下 Shader Library 和 AssetRegistry 是否存在 排序问题

  • 初步验证 Shader Library 、 AssetRegistry 存在多次构建不同的情况

UE 参与 Cook 的资源,从逻辑上大概可以分为这个类别:

  1. 项目设置中的关键必要资源

    • StartupMap
    • GameMode
    • GameInstance
    • DefaultTouchInterface
  2. 默认打包的几个 UI 目录

    哪几个? 为什么需要? 可以裁剪吗?

  3. 引擎启动时通过代码加载的资源

    eg. UClass的CDO

    1
    
    static ConstructorHelpers::FObjectFinder<UMaterialInterface> NormalMaterialRef(TEXT("/Game/Assets/UI/spineplugin/UI_SpineUnlitNormalMaterial"));
    

    这些资源在运行时有什么用?

    可以裁剪吗? => 从合理性上,不建议裁剪,可能引发 UClass的逻辑错误引发异常

  4. 项目设置中配置的 Cook 资源(包括 Directory to Alway CookPrimaryAssetLabel 等标记的要进行 Cook 的资源)

  5. 通过执行 FGameDelegates::Get().GetCookModificationDelegate() 传递给 CookOnTheFlyServer 的资源。

  6. 在一定条件下若没有指定资源,则分析项目、插件目录下的资源

    分析规则是什么?会导致多打包吗?怎么裁剪?

  7. 本地化资源(UE 支持给不同的 Culture 使用不同的资源,不过不常用)

CookCommandlet

引擎中提供了 UCookCommandlet,来实现资源的 Cook,在打包流程中,它由 UAT 拉起。默认的 Cook 命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
D:/UnrealEngine/Engine/Engine/Binaries/Win64/UE4Editor-Cmd.exe
D:/UnrealProjects/Blank425/Blank425.uproject
-run=Cook
-TargetPlatform=WindowsNoEditor
-fileopenlog
-unversioned
-abslog=D:/UnrealEngine/Engine/Engine/Programs/AutomationTool/Saved/Cook-2021.12.07-15.47.10.txt
-stdout
-CrashForUAT
-unattended
-NoLogTimes
-UTF8Output

它会执行到 UCookCommandlet 的 main 中:

Source\Editor\UnrealEd\Private\Commandlets\CookCommandlet.cpp

1
2
3
4
5
6
7
8
9
10
/* UCommandlet interface
 *****************************************************************************/

int32 UCookCommandlet::Main(const FString& CmdLineParams)
{
  COOK_STAT(double CookStartTime = FPlatformTime::Seconds());
  Params = CmdLineParams;
  ParseCommandLine(*Params, Tokens, Switches);
  // ...
}

它在经过一些参数检测后会将执行流程传递到 CookByTheBook,创建出 CookOnTheFlyServer,并调用 StartCookByTheBook

引擎打包时的资源都会在 CookOnTheFlyServer 中被 Cook,并生成 Shader 和 AssetRegistry,可以说 CookOnTheFlyServer 就是 UE 打包过程中将在编辑器中通用格式的 uasset,序列化为平台格式的过程。

Cook步骤

UE 打包时加载资源的思路是:

先找到本地 uasset 文件,将路径转换为 PackageName,进行加载,在加载时会把依赖的资源也加载了,然后将其一起 Cook。

这是cook一个资源的分析流程还是一堆资源的? 重复的依赖项是怎么处理的?

这个加载的次序有保证吗?是否会导致两次构建的次序不一致

  • 源码关键词 EInstigator

StartupPackages

CookOnTheFlyServer.cppUCookOnTheFlyServer::Initialize 中,将已经加载到内存中的资源添加到了 CookByTheBookOptions->StartupPackages 中:

这里是不是就会涉及到 垃圾资源了? 或者说 由于某些意外的代码导致 内存中包含了不需要的资源?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// void UCookOnTheFlyServer::Initialize( ECookMode::Type DesiredCookMode, ECookInitializationFlags InCookFlags, const FString &InOutputDirectoryOverride )

if (IsCookByTheBookMode())
	{
		CookByTheBookOptions = new FCookByTheBookOptions();
		for (TObjectIterator<UPackage> It; It; ++It)
		{
			if ((*It) != GetTransientPackage())
			{
				CookByTheBookOptions->StartupPackages.Add(It->GetFName());
				UE_LOG(LogCook, Verbose, TEXT("Cooker startup package %s"), *It->GetName());
			}
		}
		bHybridIterativeDebug = FParse::Param(FCommandLine::Get(), TEXT("hybriditerativedebug"));
	}

CookOnTheFlyServer 在后续的流程会将它们添加到 Cook 列表中,并会处理重定向器。

AllMaps

没有通过命令行指定任何地图的情况下会给 MapIniSections 添加 AllMaps

它是 DefaultEditor.ini 中的 Section

1
2
3
4
[AllMaps]
+Map=/Game/Maps/Login
+Map=/Game/Maps/LightSpeed
+Map=/Game/Maps/VFXTest

启动之前会全局编译一遍 GlobalShader:

\Engine\Source\Editor\UnrealEd\Private\CookOnTheFlyServer.cpp

什么是GlobalShader?全局编译的目的是什么? 能不能用按需编译?可以引入缓存机制吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void UCookOnTheFlyServer::SaveGlobalShaderMapFiles(const TArrayView<const ITargetPlatform* const>& Platforms)
{
	// we don't support this behavior
	check( !IsCookingDLC() );
	for (int32 Index = 0; Index < Platforms.Num(); Index++)
	{
		TArray<FString> Files;
		TArray<uint8> GlobalShaderMap;

		// make sure global shaders are up to date!
		FShaderRecompileData RecompileData(Platforms[Index]->PlatformName(), SP_NumPlatforms, ODSCRecompileCommand::Changed, &Files, nullptr, &GlobalShaderMap);

		check( IsInGameThread() );

		FString OutputDir = GetSandboxDirectory(RecompileData.PlatformName);

		UE_LOG(LogCook, Display, TEXT("Checking global shaders for platform %s"), *RecompileData.PlatformName);

		RecompileShadersForRemote(RecompileData, OutputDir);
	}
}

通过 GRedirectCollector 获取资源:

UE5 的流程有所改动,需要跟一下断点

UI

默认情况下,UE 会将 Engine/Config/BaseEditor.ini#L271ContentDirectories 下的目录添加到 Cook 列表中:

引擎中的默认配置如下,也可以修改 DefaultEditor.ini 添加其他的目录:

BaseEditor.ini

1
2
3
4
5
6
[UI]
; Directories specifying assets needed by Slate UI, assets in these directories are always cooked even if not referenced
+ContentDirectories=/Game/UI
+ContentDirectories=/Game/Widget
+ContentDirectories=/Game/Widgets
+ContentDirectories=/Engine/MobileResources

这些目录下的资源会被打包:CookOnTheFlyServer.cpp#L6217

CollectFilesToCook

1
2
3
4
5
6
7
  	{
		TArray<FString> UIContentPaths;
		if (GConfig->GetArray(TEXT("UI"), TEXT("ContentDirectories"), UIContentPaths, GEditorIni) > 0)
		{
			UE_LOG(LogCook, Warning, TEXT("The [UI]ContentDirectories is deprecated. You may use DirectoriesToAlwaysCook in your project settings instead."));
		}
	}

Directory to Alway cook

Project Setgings-Directory to Alway cook DirectoriesToAlwaysCook

Maps

DefaultGame.ini

1
2
[/Script/UnrealEd.ProjectPackagingSettings]
+MapsToCook=(FilePath="/Game/HandheldAR/Maps/HandheldARBlankMap")

Cultures

这里代表的并不是多语言,而是针对不同的 Culture,可以支持使用不同的资源。Asset Localization

Cultures 资源的获取代码:CookOnTheFlyServer.cpp#L6714

如以下目录下的资源,并会递归子目录:

1
/Game/L10N/en/

DefaultTouchInterface

DefaultTouchInterface 是引擎中配置的虚拟摇杆类,它可能不会被其他的资源依赖,但也需要被打包,所以在 Cook 时会单独获取它:

1
2
3
4
5
6
7
8
9
10
FConfigFile InputIni;
FString InterfaceFile;
FConfigCacheIni::LoadLocalIniFile(InputIni, TEXT("Input"), true);
if (InputIni.GetString(TEXT("/Script/Engine.InputSettings"), TEXT("DefaultTouchInterface"), InterfaceFile))
{
  if (InterfaceFile != TEXT("None") && InterfaceFile != TEXT(""))
  {
    SoftObjectPaths.Emplace(InterfaceFile);
  }
}

GetCookModificationDelegate

绑定代理,可以传递给 CookCommandlet,要进行 Cook 的文件:

1
2
3
// allow the game to fill out the asset registry, as well as get a list of objects to always cook
TArray<FString> FilesInPathStrings;
FGameDelegates::Get().GetCookModificationDelegate().ExecuteIfBound(FilesInPathStrings);

注意,传递过来的需要是 **uasset 文件的绝对路径**,并不是 /Game/xxx 等资源路径

AssetManager

通过 UAssetManager::Get().ModifyCook 函数,来访问 PrimaryAssetTypeInfo(在项目设置中的配置、PrimaryAssetLabelId 等)。

如果命令行没有显式指定任何资源、以及从 FGameDelegates::Get().GetCookModificationDelegate()UAssetManager::Get().Modify 都没有获取到任何资源,则添加插件、项目所有的资源:

执行条件为:

  1. DefaultEditor.ini 中的 AlwaysCookMaps 为空,AllMaps 为空
  2. 项目设置中 List of maps to include in a packaged build 为空
  3. 项目设置中 DirectoriesToAlwaysCook 为空
  4. FGameDelegates::Get().GetCookModificationDelegate() 获取到的资源为空
  5. UAssetManager::Get().ModifyCook 获取到的资源为空

则会通过 NormailizePackageNames 获取 /Engine/Game 以及所有启用插件的 umap、uasset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// If no packages were explicitly added by command line or game callback, add all maps
if (FilesInPath.Num() == InitialPackages.Num() || bCookAll)
{
  TArray<FString> Tokens;
  Tokens.Empty(2);
  Tokens.Add(FString("*") + FPackageName::GetAssetPackageExtension());
  Tokens.Add(FString("*") + FPackageName::GetMapPackageExtension());

  uint8 PackageFilter = NORMALIZE_DefaultFlags | NORMALIZE_ExcludeEnginePackages | NORMALIZE_ExcludeLocalizedPackages;
  if (bMapsOnly)
  {
    PackageFilter |= NORMALIZE_ExcludeContentPackages;
  }

  if (bNoDev)
  {
    PackageFilter |= NORMALIZE_ExcludeDeveloperPackages;
  }

  // assume the first token is the map wildcard/pathname
  TArray<FString> Unused;
  for (int32 TokenIndex = 0; TokenIndex < Tokens.Num(); TokenIndex++)
  {
    TArray<FString> TokenFiles;
    if (!NormalizePackageNames(Unused, TokenFiles, Tokens[TokenIndex], PackageFilter))
    {
      UE_LOG(LogCook, Display, TEXT("No packages found for parameter %i: '%s'"), TokenIndex, *Tokens[TokenIndex]);
      continue;
    }

    for (int32 TokenFileIndex = 0; TokenFileIndex < TokenFiles.Num(); ++TokenFileIndex)
    {
      AddFileToCook(FilesInPath, TokenFiles[TokenFileIndex]);
    }
  }
}

但默认添加了过滤器,会排除引擎目录的资源(/Engine)以及本地化目录下的资源(/*/L10N/)。

其实就是会添加项目、插件、除了 L10N 目录下的所有 uasset 和 umap。

收集打包的内容主要在 UCookOnTheFlyServer::CollectFilesToCook 这个函数中:CookOnTheFlyServer.cpp#L5200 中。

DefaultInput.ini

1
2
[/Script/Engine.InputSettings]
DefaultTouchInterface=/Engine/MobileResources/HUD/LeftVirtualJoystickOnly.LeftVirtualJoystickOnly

Shader Cook流程

入口:UCookCommandlet::CookByTheBook

  • UCookOnTheFlyServer::StartCookByTheBook

    初始化,构建任务队列

    • InitShaderCodeLibrary

      • FShaderCodeLibrary::InitForCooking

        • FShaderCodeLibraryImpl::FShaderCodeLibraryImpl

          初始化ShaderCodeLibrary,这是一个静态实例

      • OpenShaderCodeLibrary(LibraryName)

        • FShaderCodeLibrary::OpenLibrary(LibraryName)

          • FShaderCodeLibraryImp::OpenLibrary(LibraryName)

            对每个目标平台,调用 FEditorShaderCodeArchive::OpenLiabrary(LibraryName) 准备后续保存Shader

          • 构建任务队列CookRequests

        • 主循环

          • UCookOnTheFlyServer::TickCookOnTheSide

            不断取出CookRequests 队列的材质,触发编译;

            这里因为是异步,那么就会存在次序的问题

            • UMaterial::BeginCacheForCookedPlatformData

              (见下文”材质编译”小节)

              • FMaterial::BeginCompileShaderMap

                • 把Shader编译任务加入到GShaderCompilingManager的队列中,异步编译(见下文”编译触发”小节)
                • 这个过程中会检查DDC,如果存在,直接从DDC中获取shader code
                • 异步编译,使用HLSLCC调用各个平台对应的后端进行处理(见下文”编译进行”小节)
              • FShaderCompilingManager::ProcessAsyncResults

                检查GShaderCompilingManager异步编译的结果(见下文”编译完成”小节)

                • FShaderCompilingManager::ProcessCompiledShaderMaps

                  把完成编译的Shader加入DDC

              • 如果发现材质Cook完成,会通过SaveCookedPackages调用UMaterial::Serialize (见下文”材质序列化”小节)

                • FShaderResource::SerializeShaderCode

                  把完成编译的Shader加入ShaderCodeLibrary

              • 所有Cook完成后,调用UCookOnTheFlyServer::SaveShaderCodeLibrary (见下文”ShaderLibrary序列化流程”小节)

                • FShaderCodeLibrary::SaveShaderCodeLibrary

                  序列化ShaderCodeLibrary

                • FShaderCodeLibrary::PackageNativeShaderLibrary

                  把ShaderCodeLibrary编译到native格式,仅iOS

                  • FShaderCodeLibraryImp::PackageNativeShaderLibrary

                    • FEditorShaderCodeArchive::PackageNativeShaderLibrary

                      • IShaderFormatArchive::Finalize

                        最终序列化到Package中

材质Cook流程

分两个阶段:

  • 编译阶段:CookServer的TickCookOnTheSide会不断触发材质Cook,材质Cook又会触发Shader编译。
  • 序列化阶段:也发生在TickCookOnTheSide函数内,这时会取出Cook完成的材质进入序列化。

这两个阶段都会发生ShaderMap的序列化,第一次序列化到DDC,第二次序列化到材质Package。

在DDC存在的情况下,会跳过编译。

需要注意,第一次序列化时,Shader Code进入DDC,第二次序列化时,Shader Code进入ShaderCodeLibrary。

材质和Shader的结构

  • UMaterial和UMaterialInstance是材质资源,在Game Thread加载和使用
  • FMaterial是渲染线程的材质资源,在Render Thread使用
  • FMaterialResource是FMaterial的包装,提供序列化、初始化等功能
  • 一个UMaterial可以包含多个FMaterialResource,使用Quality Level和Feature Level两级索引来管理
  • 一个FMaterial对应一个FMaterialShaderMap
  • FMaterialShaderMap使用Vertex Factory Type和Shader Type两级索引来管理Shader
  • 一个Shader对应一个FShaderResource,而FShaderResource是FShader的包装
  • FShaderResource负责引用和初始化RHI资源,提供给渲染命令使用

材质编译

入口:UMaterial::BeginCacheForCookedPlatformData

  • CacheResourceShadersForCooking
    • 遍历需要的QualityLevel,分配FMaterialResource
    • CacheShadersForResources
      • 遍历FMaterialResource,检查是否需要Cook(首先根据FeatureLevel,其次分析材质使用的QualitySwitchNode,其次判断材质是否有QualityOverride)
        • FMaterialResource::CacheShaders
          • FMaterial::CacheShaders
            • FMaterialShaderMap::LoadFromDerivedDataCache
              • 如果找到了,从DDC里面反序列化
            • 如果DDC里不存在,则调用FMaterial::BeginCompileShaderMap启动编译(见下文”编译触发”小节)
          • 编译完成后,等待CookOnTheFlyServer调用ProcessAsyncResults
            • FShaderCompilingManager::ProcessCompiledShaderMaps(见下文”编译完成”小节)
              • 把Cache好的一系列FMaterialResource存到CachedMaterialResourcesForCooking

材质序列化

入口:UMaterial::Serialize

  • SerializeInlineShaderMaps
    • 遍历CachedMaterialResourcesForCooking 中的每一个FMaterialResource
      • FMaterialShaderMap::Serialize
        • 遍历 SortedMeshShaderMaps(每个对应一种VF)
          • FMeshMaterialShaderMap::SerializeInline
            • 遍历所有shader
              • SerializeShaderForSaving
                • FShaderResource::SerializeShaderCode
                  • FShaderCodeLibrary::AddShaderCode 把shader code保存到shader code library
                • FShaderCodeLibrary找到对应的EditorShaderCodeArchive,加入其中

Shader编译流程

除了GlobalShader,大部分Shader编译都是异步的。

首先会由编辑器或者CookCommandlet触发Shader编译,编译任务由GShaderCompilingManager负责分配给workers,并不断将完成编译的ShaderMap序列化到DDC。

Shader的编译通过下面的流程把材质转换为目标平台可以识别的代码:

  1. 翻译材质蓝图为HLSL
  2. 设置材质参数(宏定义)
  3. 设置VertexFactory参数(宏定义)
  4. 设置Shader参数(宏定义)
  5. 把HLSL编译成目标平台使用的Shader格式

编译触发

  • FMaterial::BeginCompileShaderMap
    • 构建一个新的FMaterialShaderMap
    • FHLSLMaterialTranslator::Translate 把材质翻译成HLSL
    • FHLSLMaterialTranslator::GetMaterialEnvironment 获取材质特性相关的设置 -> Input.SharedEnvironment
    • FMaterialShaderMap::Compile 把HLSL编译成平台相关的shader
      • FMaterial::SetupMaterialEnvironment 获取材质渲染相关的设置 -> Input.SharedEnvironment
      • 遍历所有VertexFactory,取得VF对应的MeshShaderMap
        • FMeshMaterialShaderMap::BeginCompile
          • 遍历所有ShaderType,使用ShouldCacheMeshShader 判断ShaderType是否需要编译
            • FMeshMaterialShaderType::BeginCompileShader
              • 构建一个NewJob
              • FVertexFactoryType::ModifyCompilationEnvironment 获取VF相关的设置 -> Input.Environment
              • FMeshMaterialShaderType::SetupCompileEnvironment 获取Shader相关的宏 -> Input.Environment
              • GlobalBeginCompileShader
                • 构建NewJob->Input
                • 继续构建Input->Environment
                • Add(NewJob)
              • 遍历Mesh无关的Shaders,流程同上
              • 遍历Pipeline Shaders,流程同上
              • GShaderCompilingManager->AddJobs(NewJobs) 添加到全局编译队列中,等待进程异步编译

编译进行

以多进程编译OpenGL Shader为例

  • ShaderCompileWorker::ProcessCompilationJob
    • FShaderFormatGLSL::CompileShader
      • FOpenGLFrontend::CompileShader
        • SetupPerVersionCompilationEnvironment
        • 设置一系列编译器相关的宏
        • PreprocessShader 预处理:头文件替换,删除注释
        • FHlslCrossCompilerContext::Init 初始化编译器
        • FHlslCrossCompilerContext::Run 调用HLSLCC进行编译
          • RunFrontend 词法分析,语法分析(生成AST),语义分析(生成HIR)
          • RunBackend生成Main函数,代码优化
            • 这里的几个关键函数由FOpenGLBackend实现
          • FOpenGLBackend::GenerateCode 生成GLSL代码
        • BuildShaderOutput
          • 根据输出代码,生成FShaderCompilerOutput,其中包含参数map、采样数、最终代码、Hash值、编译错误等信息
          • 生成FOpenGLCodeHeader,其中包含了Shader类型、名称、参数绑定、UniformBuffer映射等运行时依赖的信息
          • 序列化FOpenGLCodeHeader到最终输出的ShaderCode中。运行时会先反序列化Header,随后才读取并编译shader实体

以Native方式编译iOS的Shader时,主要区别在于,编译的产物是Metal平台的硬件无关中间表达(IR),而非文本,其文件格式是一种Native的二进制(MetalLib)。由于MetalLib无法被引擎读取,引擎又需要在渲染提交时获取Shader的反射信息,因此UE将这个Header统一保存到额外的MetalMap文件中。

编译完成

入口FShaderCompilingManager::FinishCompilation 或 FShaderCompilingManager::ProcessAsyncResults

  • FShaderCompilingManager::ProcessCompiledShaderMaps
    • 遍历FShaderMapFinalizeResults,找到其对应的FMaterialShaderMap和FMaterial
      • FMaterialShaderMap::ProcessCompilationResults 处理整个ShaderMap的编译结果
        • ProcessCompilationResultsForSingleJob
          • 找到对应VF的FMeshMaterialShaderMap
          • FMeshMaterialShaderType::FinishCompileShader 处理单个Shader的编译结果
            • FShaderResource::FindOrCreateShaderResource
              • FShaderResource::FShaderResource
                • FShaderResource::CompressCode
                  • Code = FShaderCompilerOutput.Code 二进制Shader Code最终进入ShaderResource
                • 将新生成的Shader加入FMeshMaterialShaderMap
              • InitOrderedMeshShaderMaps 把FMeshMaterialShaderMap重新排序
              • SaveToDerivedDataCache 把编译结果序列化到DDC
                • FMaterialShaderMap::Serialize 这一步和上文“材质序列化”小节一样,区别在于这次是存到内存而非Package
                • 把序列化的结果存到DDC(包括shader code

ShaderLibrary序列化流程

几个关键的类:

  • FEditorShaderCodeArchive:Cook时使用,负责保存Shader信息
  • FShaderCodeArchive:运行时使用,负责读取Shader信息
  • IShaderFormatArchive:运行和Cook时使用,平台相关的类,负责具体的序列化和反序列工作(仅Native模式)

上述类之间的关系:

  • Cook时,Shader通过FShaderCodeLibrary,进入FEditorShaderCodeArchive
  • Cook结束后,FEditorShaderCodeArchive将所有Shader序列化到Library文件中。
    • 如果是iOS,会再通过IShaderFormatArchive,把Shader编译成metallib
  • 运行时,FShaderCodeLibrary读取多个FShaderCodeArchive,从中加载shader

总体流程

入口FShaderCodeLibraryImp::SaveShaderCode

  • FEditorShaderCodeArchive::Finalize
  • 保存ShaderHash->ShaderEntry的索引(以TMap的形式)
  • 保存所有ShaderCode

编译Native Library(仅Native模式)

  • 首先,在Shader编译阶段,会调用XCode把Shader编译成二进制的IR
  • 创建IShaderFormatArchive
  • 遍历FEditorShaderCodeArchive中所有Shader
    • Strip
    • 将Shader加入IShaderFormatArchive
      • 把Shader中编译好的二进制IR保存到中间文件夹,并且生成ShaderID
      • 把ShaderID加入Shader列表
      • 把ShaderHash加入ShaderMap
    • Finalize
      • 调用XCode把中间文件夹里的所有二进制IR链接起来

SkipEditorContent

在 CookOnTheFlyServer 中会忽略 /Engine/Editor*/Editor/VREditor* 中的资源:

1
2
3
4
5
6
7
8
// don't save Editor resources from the Engine if the target doesn't have editoronly data
if (IsCookFlagSet(ECookInitializationFlags::SkipEditorContent) &&
	(PackagePathName.StartsWith(TEXT("/Engine/Editor")) || PackagePathName.StartsWith(TEXT("/Engine/VREditor"))) &&
	!Target->HasEditorOnlyData())
{
	Result = ESavePackageResult::ContainsEditorOnlyData;
	bCookPackage = false;
}

Cook 时的依赖加载

虽然上面列出了引擎打包时将会包含的资源,但是,它们还不是全部,因为它们都还是单个的资源或者单个的目录,并没有包含资源的依赖关系。

所以,在 UE 进行 Cook 时还会进行实质上的依赖分析。

考虑以下两个问题:

  1. 如果一个地图中放了一个 C++ 实现的 Actor,而在它的构造函数代码中有加载某个资源,怎么将其打包呢? 从资源依赖的角度上,是没有依赖关系的
1
2
3
4
5
6
7
AMyActor::AMyActor()
{
  // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;

  UTexture2D* Texture2D = LoadObject<UTexture2D>(nullptr, TEXT("/Game/TextureResources/T_ImportTexture.T_ImportTexture"));
}
  1. 没有在 AssetRegistry 引用关系中的资源依赖,如何打包?如 AnimSequenceBoneCompressionSettingsCruveCompressionSettings 配置,并不在依赖关系中,从 AsssetRegistry 获取动画序列的依赖时找不到它们。

但是它们在 uasset 的 ImportTable 中有记录:

从 Cooked 的 uasset 中也可以看到:

为什么不能从 Asset 的 ImportTable 里直接扫描依赖呢?

这是因为访问 ImportTable 需要真正加载资源,当资源量非常大时,这是非常耗费硬件资源和时间的,而从 AssetRegistry 访问依赖关系则无需将资源加载到内存里,所以速度非常快。

UE 默认的实现机制:

  1. 引擎启动时会创建 CDO,会执行类的构造函数  » EInstigator::StartupPackage
  2. UE 的资源在加载时会将它依赖的资源也给加载了

UE 在 Cook 时实现了一个方案,监听所有创建的 UObject,将其添加至 Cook 列表中,确保在 C++ 的构造函数中加载的资源和通过 ImportTable 加载的依赖资源也能被 Cook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct FPackageTracker : public FUObjectArray::FUObjectCreateListener, public FUObjectArray::FUObjectDeleteListener
{
FPackageTracker::FPackageTracker(FPackageDatas& InPackageDatas)
		:PackageDatas(InPackageDatas)
	{
		for (TObjectIterator<UPackage> It; It; ++It)
		{
			UPackage* Package = *It;

			if (Package->GetOuter() == nullptr)
			{
				LoadedPackages.Add(Package);
			}
		}

		NewPackages.Reserve(LoadedPackages.Num());
		for (UPackage* Package : LoadedPackages)
		{
			NewPackages.Add(Package, FInstigator(EInstigator::StartupPackage));
		}

		GUObjectArray.AddUObjectDeleteListener(this);
		GUObjectArray.AddUObjectCreateListener(this);
	}

	FPackageTracker::~FPackageTracker()
	{
		GUObjectArray.RemoveUObjectDeleteListener(this);
		GUObjectArray.RemoveUObjectCreateListener(this);
	}
    
    virtual void NotifyUObjectCreated(const class UObjectBase* Object, int32 Index) override;
		virtual void NotifyUObjectDeleted(const class UObjectBase* Object, int32 Index) override;
    
}

所以,基于相同的思路,我们只需要通过 AssetRegistry 获取资源的依赖关系,存储一个粗略的资源列表,然后在 Cook 时监听 UObject 的创建,若不在扫描的资源列表中,就将其添加至 Cook 队列,从而实现完整的资源打包过程。

UCookOnTheFlyServer::CookByTheBookFinished

创建 AssetRegister.bin

本文由作者按照 CC BY 4.0 进行授权