UE4.27.2 PAK 文件运行时加载的调试分析
2025-05-20 22:58:09

分析目标:挂载了 pak 之后怎么加载其中的资产?

脑子里暂时比较乱,在网上查了一些东西也没看懂

AssetRegistry.bin 是干什么的,为什么我在 Cooked 文件夹中看到了它,但是在我的 pak 文件的解压结果中却没有?它会影响我挂载 pak 时的资源查找吗?

AssetRegistry.ScanPathsSynchronous 实际干了什么?为什么我挂载了 pak 之后,调用了它,我还是找不到我 pak 中的资产?

FPakPlatformFile::MountAllPakFiles 是游戏开始时加载所有 pak 的入口,这个内部也是调用 FPakPlatformFile::Mount

LoadObject 怎么找到这些文件的?

挂载 PAK 文件

通过 FPakPlatformFile::Mount 来挂载 pak 文件

前半部分是创建 FPakFile 对象,加到 FPakPlatformFile 自己的数组里面

1
TRefCountPtr<FPakFile> Pak = new FPakFile(LowerLevel, InPakFilename, bSigned, bLoadIndex);

那么其实这里相当于仅仅是创建对象而已

要么具体从磁盘加载到 package 的逻辑在后面,要么在 FPakFile 构造函数里面

那么可以看到 FPakFile 构造函数调用了 FPakFile::Initialize。这里面也仅仅是读取 pak 文件信息,而不是读取所有内容。

于是看到 FPakPlatformFile::Mount 的后面的部分

1
2
3
4
5
6
if (FIoDispatcher::IsInitialized())
{
FIoStoreEnvironment IoStoreEnvironment;
IoStoreEnvironment.InitializeFileEnvironment(FPaths::ChangeExtension(InPakFilename, FString()), PakOrder);
FIoStatus IoStatus = FIoDispatcher::Get().Mount(IoStoreEnvironment, EncryptionKeyGuid, EncryptionKey);
}

FIoDispatcher::Mount -> FIoDispatcherImpl::Mount -> FFileIoStore::Mount

于是看到是要加载 utoc 文件

这跟我想看的不一样

所以我还是没有看到 pak mount 之后,是怎么影响 package 的

LoadObject

LoadObjectStaticLoadObject 的一层包装,只是转换参数,没做别的事

StaticLoadObjectStaticLoadObjectInternal 的一层包装,检查了线程状态和返回值

StaticLoadObjectInternal 决定了从哪里加载对象。

如果允许重用且名称完整,先尝试在内存中查找对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
		// If we have a full UObject name then attempt to find the object in memory first,
if (bAllowObjectReconciliation && (bContainsObjectName
#if WITH_EDITOR
|| GIsImportingT3D
#endif
))
{
Result = StaticFindObjectFast(ObjectClass, InOuter, *StrName);
if (Result && Result->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad | RF_NeedPostLoadSubobjects | RF_WillBeLoaded))
{
// Object needs loading so load it before returning
Result = nullptr;
}
}

如果对象未找到且Outer所在包不是编译进来的,加载整个包。

1
2
3
4
5
6
7
if (!Result)
{
if (!InOuter->GetOutermost()->HasAnyPackageFlags(PKG_CompiledIn))
{
// now that we have one asset per package, we load the entire package whenever a single object is requested
LoadPackage(NULL, *InOuter->GetOutermost()->GetName(), LoadFlags & ~LOAD_Verify, nullptr, InstancingContext);
}

包加载后再次查找对象。

1
2
3
4
5
6
// now, find the object in the package
Result = StaticFindObjectFast(ObjectClass, InOuter, *StrName);
if (GEventDrivenLoaderEnabled && Result && Result->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad | RF_NeedPostLoadSubobjects | RF_WillBeLoaded))
{
UE_LOG(LogUObjectGlobals, Fatal, TEXT("Return an object still needing load from StaticLoadObjectInternal %s"), *GetFullNameSafe(Result));
}

如果没找到对象,且允许重定向,尝试查找重定向器

1
2
3
4
5
6
7
8
9
// If the object was not found, check for a redirector and follow it if the class matches
if (!Result && !(LoadFlags & LOAD_NoRedirects))
{
UObjectRedirector* Redirector = FindObjectFast<UObjectRedirector>(InOuter, *StrName);
if (Redirector && Redirector->DestinationObject && Redirector->DestinationObject->IsA(ObjectClass))
{
return Redirector->DestinationObject;
}
}

如果没有找到对象,且名称不包含.,则假设对象是包内主资产,构造完整名称后递归调用自身加载。

1
2
3
4
5
6
7
8
9
if (!Result && !bContainsObjectName)
{
// Assume that the object we're trying to load is the main asset inside of the package
// which usually has the same name as the short package name.
StrName = InName;
StrName += TEXT(".");
StrName += FPackageName::GetShortName(InName);
Result = StaticLoadObjectInternal(ObjectClass, InOuter, *StrName, Filename, LoadFlags, Sandbox, bAllowObjectReconciliation, InstancingContext);
}

LoadPackage

挣扎于 LoadPackageInternal 的前半部分

LoadPackageLoadPackageInternal 的包装,该包装添加了 debug 信息

LoadPackageInternal 内部处理包装名参数 InLongPackageNameOrFilename,然后调用 LoadPackageAsync 去异步加载包

LoadPackageAsync 是发起加载请求的包装。该包装获取 IAsyncPackageLoader 单例调用 FAsyncLoadingThread::LoadPackage

FAsyncLoadingThread::LoadPackage 把请求入队

1
2
3
4
5
6
7
// Add new package request
FAsyncPackageDesc PackageDesc(RequestID, *PackageName, *PackageNameToLoad, InGuid ? *InGuid : FGuid(), MoveTemp(CompletionDelegatePtr), InPackageFlags, InPIEInstanceID, InPackagePriority);
if (InstancingContext)
{
PackageDesc.SetInstancingContext(*InstancingContext);
}
QueuePackage(PackageDesc);

入队的存储在 QueuedPackages

通过查找引用可以看到 FAsyncLoadingThread::CreateAsyncPackagesFromQueue 处理了 QueuedPackages

FAsyncLoadingThread::ProcessAsyncLoadingFAsyncLoadingThread::ProcessAsyncLoadingFAsyncLoadingThread::TickAsyncThread 调用

对于我目前的调试,在 FAsyncLoadingThread::ProcessAsyncLoading 打断点,可以看到是 FAsyncLoadingThread::TickAsyncThread 调用了它

来源是 LoadPackageInternal 调用了一个 flush 方法,这个方法最终调用到 FAsyncLoadingThread::FlushLoading, 它调用到 TickAsyncThread

那么继续看 FAsyncLoadingThread::ProcessAsyncPackageRequest 是怎么处理包加载的

先在AsyncPackageNameLookup(异步加载队列的包名查找表)中查找是否已有对应包。

如果找到,说明包已经在异步加载队列中,调用UpdateExistingPackagePriorities确保该包及其依赖包的优先级至少和当前请求的优先级一样高,避免优先级倒置。

1
2
3
4
5
6
7
8
FAsyncPackage* Package = FindExistingPackageAndAddCompletionCallback(InRequest, AsyncPackageNameLookup, FlushTree);

if (Package)
{
// The package is already sitting in the queue. Make sure the its priority, and the priority of all its
// dependencies is at least as high as the priority of this request
UpdateExistingPackagePriorities(Package, InRequest->Priority);
}

如果未找到,尝试在已加载包集合中查找

1
2
3
4
5
6
7
8
9
10
11
12
if (Package)
{
...
}
else
{
// [BLOCKING] LoadedPackages are accessed on the main thread too, so lock to be able to add a completion callback
#if THREADSAFE_UOBJECTS
FScopeLock LoadedLock(&LoadedPackagesCritical);
#endif
Package = FindExistingPackageAndAddCompletionCallback(InRequest, LoadedPackagesNameLookup, FlushTree);
}

如果仍未找到,尝试在待处理加载包集合中查找

1
2
3
4
5
6
7
8
if (!Package)
{
// [BLOCKING] LoadedPackagesToProcess are modified on the main thread, so lock to be able to add a completion callback
#if THREADSAFE_UOBJECTS
FScopeLock LoadedLock(&LoadedPackagesToProcessCritical);
#endif
Package = FindExistingPackageAndAddCompletionCallback(InRequest, LoadedPackagesToProcessNameLookup, FlushTree);
}

如果依然没找到,创建新包并加入队列

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
if (!Package)
{
// New package that needs to be loaded or a package has already been loaded long time ago
{
// GC can't run in here
FGCScopeGuard GCGuard;
Package = new FAsyncPackage(*this, *InRequest, EDLBootNotificationManager);
}
if (InRequest->PackageLoadedDelegate.IsValid())
{
const bool bInternalCallback = false;
Package->AddCompletionCallback(MoveTemp(InRequest->PackageLoadedDelegate), bInternalCallback);
}
Package->SetDependencyRootPackage(InRootPackage);
if (FlushTree)
{
Package->PopulateFlushTree(FlushTree);
}

// Add to queue according to priority.
InsertPackage(Package, false, EAsyncPackageInsertMode::InsertAfterMatchingPriorities);

// For all other cases this is handled in FindExistingPackageAndAddCompletionCallback
const int32 QueuedPackagesCount = QueuedPackagesCounter.Decrement();
NotifyAsyncLoadingStateHasMaybeChanged();
check(QueuedPackagesCount >= 0);
}

这里的逻辑也只有一个 InsertPackage 把包插入到 AsyncPackages

我还是没有看到包是怎么被查找的

重新看 LoadPackageInternal

1
2
3
4
5
6
7
8
9
int32 RequestID = LoadPackageAsync(InName, nullptr, *InPackageName);

if (RequestID != INDEX_NONE)
{
FlushAsyncLoading(RequestID);
}

Result = (InOuter ? InOuter : FindObjectFast<UPackage>(nullptr, PackageFName));
return Result;

他这里还是进入了 FindObjectFast

fine,最终还是没有看到是怎么创建的

之后去看了一些别人的分析,我才发现,这个 LoadPackage 不负责加载资源

它更多是负责抽象出来资源包的状态管理?比如管理加载这个状态之类的?但是真正的加载函数,他也是调用别人的?

为什么一直留在 LoadPackageInternal 前半部分

其实我前面一直困顿于这段代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
if ((FPlatformProperties::RequiresCookedData() && GEventDrivenLoaderEnabled
&& EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME)
#if WITH_IOSTORE_IN_EDITOR
|| FIoDispatcher::IsInitialized()
#endif
)
{
FString InName;
FString InPackageName;

if (FPackageName::IsPackageFilename(InLongPackageNameOrFilename))
{
FPackageName::TryConvertFilenameToLongPackageName(InLongPackageNameOrFilename, InPackageName);
}
else
{
InPackageName = InLongPackageNameOrFilename;
}

if (InOuter)
{
InName = InOuter->GetPathName();
}
else
{
InName = InPackageName;
}

FName PackageFName(*InPackageName);
#if WITH_IOSTORE_IN_EDITOR
// Use the old loader if an uncooked package exists on disk
const bool bDoesUncookedPackageExist = FPackageName::DoesPackageExist(InPackageName, nullptr, nullptr, true) && !DoesPackageExistInIoStore(FName(*InPackageName));
if (!bDoesUncookedPackageExist)
#endif
{
if (FCoreDelegates::OnSyncLoadPackage.IsBound())
{
FCoreDelegates::OnSyncLoadPackage.Broadcast(InName);
}

int32 RequestID = LoadPackageAsync(InName, nullptr, *InPackageName);

if (RequestID != INDEX_NONE)
{
FlushAsyncLoading(RequestID);
}

Result = (InOuter ? InOuter : FindObjectFast<UPackage>(nullptr, PackageFName));
return Result;
}
}

主要是因为,在我调试 standalone game 的时候,这个 if 一直为真

1
2
if ((FPlatformProperties::RequiresCookedData() && GEventDrivenLoaderEnabled
&& EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME)

FPlatformProperties::RequiresCookedData() 会为真,它是取 template 参数

alt text

GEventDrivenLoaderEnabled 等宏也是类似

我这里查找的 result 总是 null

LoadPackageInternal 开头打断点,正确挂载了 pak file 时,没有触发断点

尝试加载一个刚刚挂载的 pack pak 中的蓝图类型,一下子就找到了 package

看来,package 的更新都不是都走的 LoadPackageInternal 的流程

LoadPackageInternal 的后面的部分

后面的部分就可以看到,核心是调用 Linker->LoadAllObjects

1
2
3
4
5
6
7
8
9
10
Linker = GetPackageLinker(InOuter, *FileToLoad, LoadFlags, nullptr, nullptr, InReaderOverride, &InOutLoadContext, ImportLinker, InstancingContext);

...

if ((LoadFlags & DoNotLoadExportsFlags) == 0)
{
Linker->LoadAllObjects(GEventDrivenLoaderEnabled);

...
}

GetPackageLinker 里面就是从 package 查找 linker,如果找不到,就创建 linker,并且把它关联到这个 package

所以是 package 拥有 linker

FLinkerLoad 被创建之后立即 Tick 一次

看别人博客说是,LoadPackageIntenal() 中会根据路径创建一个对应的 FLinkerLoad,它被创建完后会马上执行自身的 Tick()

在哪里?

于是看到 GetPackageLinker -> FLinkerLoad::CreateLinker

这里确实是,先 FLinkerLoad::CreateLinkerAsyncLinker->Tick

FLinkerLoad

导入导出表

FLinkerLoad 继承自 FLinkerTablesFLinkerTables 里面就有网上教程里面经常提到的 uasset 结构中的导入表导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
class FLinkerTables
{
public:
/** The list of FObjectImports found in the package */
TArray<FObjectImport> ImportMap;
/** The list of FObjectExports found in the package */
TArray<FObjectExport> ExportMap;
/** List of dependency lists for each export */
TArray<TArray<FPackageIndex> > DependsMap;
/** List of packages that are soft referenced by this package */
TArray<FName> SoftPackageReferenceList;
/** List of Searchable Names, by object containing them. Not in MultiMap to allow sorting, and sizes are usually small enough where TArray makes sense */
TMap<FPackageIndex, TArray<FName> > SearchableNamesMap;

FLinkerLoad::Tick

因为是创建之后先调用一次 Tick 所以先看这个

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* Ticks an in-flight linker and spends InTimeLimit seconds on creation. This is a soft time limit used
* if bInUseTimeLimit is true.
*
* @param InTimeLimit Soft time limit to use if bInUseTimeLimit is true
* @param bInUseTimeLimit Whether to use a (soft) timelimit
* @param bInUseFullTimeLimit Whether to use the entire time limit, even if blocked on I/O
*
* @return true if linker has finished creation, false if it is still in flight
*/
FLinkerLoad::ELinkerStatus FLinkerLoad::Tick( float InTimeLimit, bool bInUseTimeLimit, bool bInUseFullTimeLimit, TMap<TPair<FName, FPackageIndex>, FPackageIndex>* ObjectNameWithOuterToExportMap)
{
ELinkerStatus Status = LINKER_Loaded;

if( bHasFinishedInitialization == false )
{
// Store variables used by functions below.
TickStartTime = FPlatformTime::Seconds();
bTimeLimitExceeded = false;
bUseTimeLimit = bInUseTimeLimit;
bUseFullTimeLimit = bInUseFullTimeLimit;
TimeLimit = InTimeLimit;

do
{
bool bCanSerializePackageFileSummary = false;
if (GEventDrivenLoaderEnabled)
{
check(Loader || bDynamicClassLinker);
bCanSerializePackageFileSummary = true;
}
else
{
// Create loader, aka FArchive used for serialization and also precache the package file summary.
// false is returned until any precaching is complete.
SCOPED_LOADTIMER(LinkerLoad_CreateLoader);
Status = CreateLoader(TFunction<void()>([]() {}));

bCanSerializePackageFileSummary = (Status == LINKER_Loaded);
}

// Serialize the package file summary and presize the various arrays (name, import & export map)
if (bCanSerializePackageFileSummary)
{
SCOPED_LOADTIMER(LinkerLoad_SerializePackageFileSummary);
Status = SerializePackageFileSummary();
}

// Serialize the name map and register the names.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_SerializeNameMap);
Status = SerializeNameMap();
}

// Serialize the gatherable text data map.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_SerializeGatherableTextDataMap);
Status = SerializeGatherableTextDataMap();
}

// Serialize the import map.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_SerializeImportMap);
Status = SerializeImportMap();
}

// Serialize the export map.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_SerializeExportMap);
Status = SerializeExportMap();
}

#if WITH_TEXT_ARCHIVE_SUPPORT
// Reconstruct the import and export maps for text assets
if (Status == LINKER_Loaded)
{
SCOPED_LOADTIMER(LinkerLoad_ReconstructImportAndExportMap);
Status = ReconstructImportAndExportMap();
}
#endif

// Fix up import map for backward compatible serialization.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_FixupImportMap);
Status = FixupImportMap();
}

// Populate the linker instancing context for instance loading if needed.
if (Status == LINKER_Loaded)
{
SCOPED_LOADTIMER(LinkerLoad_PopulateInstancingContext);
Status = PopulateInstancingContext();
}

// Fix up export map for object class conversion
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_FixupExportMap);
Status = FixupExportMap();
}

// Serialize the dependency map.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_SerializeDependsMap);
Status = SerializeDependsMap();
}

// Hash exports.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_CreateExportHash);
Status = CreateExportHash();
}

// Find existing objects matching exports and associate them with this linker.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_FindExistingExports);
Status = FindExistingExports();
}

if (Status == LINKER_Loaded)
{
SCOPED_LOADTIMER(LinkerLoad_SerializePreloadDependencies);
Status = SerializePreloadDependencies();
}

// Finalize creation process.
if( Status == LINKER_Loaded )
{
SCOPED_LOADTIMER(LinkerLoad_FinalizeCreation);
Status = FinalizeCreation(ObjectNameWithOuterToExportMap);
}
}
// Loop till we are done if no time limit is specified, or loop until the real time limit is up if we want to use full time
while (Status == LINKER_TimedOut &&
(!bUseTimeLimit || (bUseFullTimeLimit && !IsTimeLimitExceeded(TEXT("Checking Full Timer"))))
);
}

if (Status == LINKER_Failed)
{
LinkerRoot->LinkerLoad = nullptr;
#if WITH_EDITOR

if (LoadProgressScope)
{
delete LoadProgressScope;
LoadProgressScope = nullptr;
}
#endif
}

// Return whether we completed or not.
return Status;
}

这个看得就很爽,每一步要做什么都很清楚

FLinkerLoad::LoadAllObjects

于是看到 FLinkerLoad::LoadAllObjects 预期这里是实现了怎么从磁盘加载内容到内存中

他的核心部分

1
2
3
4
5
6
7
8
9
10
11
12
13
UObject* LoadedObject = CreateExportAndPreload(ExportIndex, bForcePreload);

if(!GEventDrivenLoaderEnabled || !EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME)
{
// DynamicClass could be created without calling CreateImport. The imported objects will be required later when a CDO is created.
if (UDynamicClass* DynamicClass = Cast<UDynamicClass>(LoadedObject))
{
for (int32 ImportIndex = 0; ImportIndex < ImportMap.Num(); ++ImportIndex)
{
CreateImport(ImportIndex);
}
}
}

就很短,但是这里居然没有一个函数名明显地写从磁盘加载

于是还是要挨个函数看

FLinkerLoad::CreateExportAndPreload

也很短

1
2
3
4
5
6
7
8
9
10
UObject* FLinkerLoad::CreateExportAndPreload(int32 ExportIndex, bool bForcePreload /* = false */)
{
UObject *Object = CreateExport(ExportIndex);
if (Object && (bForcePreload || dynamic_cast<UClass*>(Object) || Object->IsTemplate() || dynamic_cast<UObjectRedirector*>(Object)))
{
Preload(Object);
}

return Object;
}

第一印象是首先创建导出表,然后 load 一些东西

于是进来看到 FLinkerLoad::Preload

FLinkerLoad::Preload

前面一些检查就不说了

定位并读取对象数据

1
2
3
4
5
6
int32 const ExportIndex = Object->GetLinkerIndex();
FObjectExport& Export = ExportMap[ExportIndex];
check(Export.Object==Object);

const int64 SavedPos = Loader->Tell();
Seek(Export.SerialOffset);

这部分的内容是

获取对象在导出表中的索引和导出信息

保存当前文件读取位置

定位到对象数据在文件中的偏移

然后有一些预缓存数据

1
2
3
4
5
6
7
8
9
FAsyncArchive* AsyncLoader = GetAsyncLoader();
if (AsyncLoader)
{
AsyncLoader->PrecacheWithTimeLimit(...);
}
else
{
Loader->Precache(...);
}

暂时不知道是干什么的

然后应该就是反序列化对象数据的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (Object->HasAnyFlags(RF_ClassDefaultObject))
{
// Maintain the current SerializedObjects.
UObject* PrevSerializedObject = CurrentLoadContext->SerializedObject;
CurrentLoadContext->SerializedObject = Object;
Object->GetClass()->SerializeDefaultObject(Object, *this);
}
else
{
// Maintain the current SerializedObjects.
UObject* PrevSerializedObject = CurrentLoadContext->SerializedObject;
CurrentLoadContext->SerializedObject = Object;
Object->Serialize(*this);
}

这其中还区分了是否是 CDO

这里还涉及到递归调用 Preload,也就是递归加载

涉及到通过延迟加载来解决循环依赖的问题,就不看了

总之,这里最终调用到 UObject::Serialize

UObject::Serialize

各个博客已经讲得很清楚,UObject::Serialize 实现了访问者模式

也就是它只调用一个算子,各个访问者重载这个算子,就可以实现不同的访问者有不同的逻辑

这里应该是,输入 FLinkerLoad 就是读取 uasset(磁盘)到内存,输入 FLinkerSave 就是从内存保存到 uasset(磁盘)

该函数内部最主要的就是调用了 UObject::SerializeScriptProperties,其他的逻辑比如确保预加载,事务(Undo/Redo)支持等,就不看了

于是看 UObject::SerializeScriptProperties

回顾

看到别人对相关代码的反序列化分析

深入理解UE4:反序列化流程详细分析

我之后也不想自己看实际 FMemory::Memcpy 读数据的部分了,就看这篇文章大概了解,在 FArchiveFileReaderGeneric::Serialize

因为感觉到这里,已经和 pak 加载相隔很远了

而且其实我前面看代码,虽然大概看了每个函数都在干什么,但是不是很懂整体关系

看了这个文章,大概知道,回顾 FLinkerLoad::LoadAllObjects 他的核心部分

1
2
3
4
5
6
7
8
9
10
11
12
13
UObject* LoadedObject = CreateExportAndPreload(ExportIndex, bForcePreload);

if(!GEventDrivenLoaderEnabled || !EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME)
{
// DynamicClass could be created without calling CreateImport. The imported objects will be required later when a CDO is created.
if (UDynamicClass* DynamicClass = Cast<UDynamicClass>(LoadedObject))
{
for (int32 ImportIndex = 0; ImportIndex < ImportMap.Num(); ++ImportIndex)
{
CreateImport(ImportIndex);
}
}
}

先创建 export 对象,为 export 对象预先加载一些值,这个时候所有的 export 对象就是摸具就创建完了

这个时候再遍历模具,对每一个模具 import,就完成了对象最终的加载

我大概的概念是这样,但是我还是不知道 pak 加载之后对象是怎么被加载的

先尝试 ReloadPackages

我一开始看的是

Engine/Source/Editor/UnrealEd/Private/PackageTools.cpp

1
bool UPackageTools::ReloadPackages( const TArray<UPackage*>& TopLevelPackages, FText& OutErrorMessage, const EReloadPackagesInteractionMode InteractionMode )

一开始只是想不求甚解直接用,但是发现它是 UnrealEd 模块,不可以编译到非 Editor 的模块中

于是仔细看他的函数,发现他也只是把传入的 package 列表放在一起,然后把 in memory 的 package 挑出来不重载

然后再判断要处理的包是否在当前世界中,如果在的话,当前世界就要先卸载,等包 reload 之后,世界再重新加载

之后就是具体处理 package reload 的逻辑

Engine/Source/Runtime/CoreUObject/Private/UObject/PackageReload.cpp

1
void ReloadPackages(const TArrayView<FReloadPackageData>& InPackagesToReload, TArray<UPackage*>& OutReloadedPackages, int32 InNumPackagesPerBatch)

这个函数在 CoreUObject 模块,所以还是可以在游戏中使用的!

这里面只传入了一个 package 对象,但是代码里面是明显有新旧包的概念

要被替换的旧包被 PackageReloadInternal::ValidateAndPreparePackageForReload 处理。

1
2
3
4
5
for (const FReloadPackageData& PackageToReloadData : InPackagesToReload)
{
PreparingPackagesForReloadSlowTask.EnterProgressFrame(1.0f);
ExistingPackages.Refs.Emplace(PackageReloadInternal::ValidateAndPreparePackageForReload(PackageToReloadData.PackageToReload));
}

该函数首先对 package 做 validation:如果这个 package 是 in memory only 的,也就是仅存在内存中的,那么就没有从磁盘重新加载这一说了

然后把 package 的 FLinkerLoad 成员重置,相当于断开了这个 package 和旧的磁盘数据之间的桥梁

新包概念出现的位置在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PackageReloadInternal::FNewPackageReferences NewPackages;
NewPackages.Refs.Reserve(ExistingPackages.Refs.Num());

for (; PackageIndex < ExistingPackages.Refs.Num(); ++PackageIndex)
{
UPackage* ExistingPackage = ExistingPackages.Refs[PackageIndex].RawRef;

// ...

NewPackages.Refs.Emplace(PackageReloadInternal::LoadReplacementPackage(ExistingPackage, InPackagesToReload[PackageIndex].LoadFlags));

UPackage* NewPackage = NewPackages.Refs[PackageIndex].Package;
NewPackages.Refs[PackageIndex].EventData = PackageReloadInternal::GeneratePackageReloadEvent(ExistingPackage, NewPackage);
}

PackageReloadInternal::LoadReplacementPackage 里面涉及到替换的逻辑,主要是两个

一个是把旧包重命名

1
2
3
4
5
6
7
8
9
10
11
const ERenameFlags PkgRenameFlags = REN_ForceNoResetLoaders | REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional | REN_SkipGeneratedClasses;
InExistingPackage->Rename(
*MakeUniqueObjectName(
Cast<UPackage>(InExistingPackage->GetOuter()),
UPackage::StaticClass(),
*FString::Printf(TEXT("%s_DEADPACKAGE"), *InExistingPackage->GetName())
).ToString(),
nullptr,
PkgRenameFlags
);
MarkPackageReplaced(InExistingPackage);

一个是加载新包

1
UPackage* NewPackage = LoadPackage(Cast<UPackage>(InExistingPackage->GetOuter()), *ExistingPackageName, InLoadFlags);

此外还有逻辑来处理加载新包时的失败逻辑,这里就不看了

重新反序列化对象的逻辑

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
// Main pass to go through and fix-up any references pointing to data from the old package to point to data from the new package
// todo: multi-thread this like FHotReloadModule::ReplaceReferencesToReconstructedCDOs?
for (FThreadSafeObjectIterator ObjIter(UObject::StaticClass(), false, RF_NoFlags, EInternalObjectFlags::PendingKill); ObjIter; ++ObjIter)
{
UObject* PotentialReferencer = *ObjIter;

// Mutating the old versions of classes can result in us replacing the SuperStruct pointer, which results
// in class layout change and subsequently crashes because instances will not match this new class layout:
UClass* AsClass = Cast<UClass>(PotentialReferencer);
if (!AsClass)
{
AsClass = PotentialReferencer->GetTypedOuter<UClass>();
}

if(AsClass)
{
if( AsClass->HasAnyClassFlags(CLASS_NewerVersionExists) ||
AsClass->HasAnyFlags(RF_NewerVersionExists))
{
continue;
}
}

PackageReloadInternal::FReplaceObjectReferencesArchive ReplaceRefsArchive(PotentialReferencer, OldObjectToNewData, ExistingPackages.Refs, NewPackages.Refs);
PotentialReferencer->Serialize(ReplaceRefsArchive); // Deal with direct references during Serialization
PotentialReferencer->GetClass()->CallAddReferencedObjects(PotentialReferencer, ReplaceRefsArchive); // Deal with indirect references via AddReferencedObjects
}

这里是遍历所有的 UObject

这个迭代器是构造的时候指向一个全局变量,它是一个存储了 UObject 的数组(内部实现还是有一些说法的,但是这里先跳过)

1
2
3
4
// Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectHash.cpp

// Global UObject array instance
FUObjectArray GUObjectArray;

首先判断对象所属的类是否标记为“有新版本存在”(CLASS_NewerVersionExists或RF_NewerVersionExists)。

这类对象通常是热重载或重新构建的类的旧版本实例,跳过它们避免因类布局变化导致崩溃。

之后的逻辑就是对于当前遍历到的 UObject 替换引用,然后反序列化

试用了一下,简直完美!直接就成功了!

运行时替换纹理导致的偶发性问题

这是一个偶发性的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FRHIResource::Release() RHIResources.h:54
TArray::~TArray<…>() Array.h:621
FD3D11UniformBuffer::~FD3D11UniformBuffer() D3D11UniformBuffer.cpp:381
<lambda_84e30748...>::operator()(TArray<…> &) RHI.cpp:599
FRHIResource::FlushPendingDeletes(bool) RHI.cpp:629
FlushPendingDeleteRHIResources_RenderThread() RenderingThread.cpp:1303
<lambda_83aa676a...>::operator()(FRHICommandListImmediate &) SceneRendering.cpp:4008
TEnqueueUniqueRenderCommandType<`FRendererModule::BeginRenderingViewFamily'::`43'::FDrawSceneCommandName,<lambda_83aa676af25f62e876425d334fdfc6e4> >::DoTask(Type,const TRefCountPtr<FGraphEvent> &) RenderingThread.h:183
TGraphTask<TEnqueueUniqueRenderCommandType<`FRendererModule::BeginRenderingViewFamily'::`43'::FDrawSceneCommandName,<lambda_83aa676af25f62e876425d334fdfc6e4> > >::ExecuteTask(TArray<FBaseGraphTask *,TSizedDefaultAllocator<32> > &,Type) TaskGraphInterfaces.h:886
FNamedTaskThread::ProcessTasksNamedThread(int, bool) TaskGraph.cpp:710
FNamedTaskThread::ProcessTasksUntilQuit(int) TaskGraph.cpp:601
FTaskGraphImplementation::ProcessThreadUntilRequestReturn(Type) TaskGraph.cpp:1480
RenderingThreadMain(FEvent *) RenderingThread.cpp:372
FRenderingThread::Run() RenderingThread.cpp:526
FRunnableThreadWin::Run() WindowsRunnableThread.cpp:84
FRunnableThreadWin::GuardedRun() WindowsRunnableThread.cpp:27
FRunnableThreadWin::_ThreadProc(void *) WindowsRunnableThread.h:37

这段报错的原因是,在 FD3D11UniformBuffer 中,有一个存储资源的 table

1
2
/** Resource table containing RHI references. */
TArray<TRefCountPtr<FRHIResource> > ResourceTable;

FD3D11UniformBuffer 销毁时,这个 TArray 也一起销毁

TArray 析构时,对每一个成员先调用析构

在这里,一个 TRefCountPtr<FRHIResource> 已经是野指针了

这个是 delete 队列里面去调用 FD3D11UniformBuffer 的析构的,于是还是要看谁发起了这个析构 FD3D11UniformBuffer

因为看到 release 方法是把 RHI 资源放入删除队列中,并且 class FD3D11UniformBuffer : public FRHIUniformBuffer, class FRHIUniformBuffer : public FRHIResource 所以应该是有人调用了 FRHIUniformBufferRelease()

我在 FRHIUniformBuffer::Release 这里加上 Log,这个 FRHIUniformBuffer::Release() 调用非常频繁,有来自 FRHICommandSetShaderUniformBuffer, FRHICommandSetGraphicsPipelineState

但是我还是不知道 ResourceTable 的成员为什么变成野指针了,所以我还不知道这个问题怎么解决