PAK热更新
PAK 热更新
文件名的内置逻辑规则
该命名为内置的逻辑,非必要规范,但需注意闭坑
name_VERSION_P.pak
- 大小写不敏感
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 (PakFilename.EndsWith(TEXT("_P.pak"))) { // Prioritize based on the chunk version number // Default to version 1 for single patch system uint32 ChunkVersionNumber = 1; FString StrippedPakFilename = PakFilename.LeftChop(6); int32 VersionEndIndex = PakFilename.Find("_", ESearchCase::CaseSensitive, ESearchDir::FromEnd); if (VersionEndIndex != INDEX_NONE && VersionEndIndex > 0) { int32 VersionStartIndex = PakFilename.Find("_", ESearchCase::CaseSensitive, ESearchDir::FromEnd, VersionEndIndex - 1); if (VersionStartIndex != INDEX_NONE) { VersionStartIndex++; FString VersionString = PakFilename.Mid(VersionStartIndex, VersionEndIndex - VersionStartIndex); if (VersionString.IsNumeric()) { int32 ChunkVersionSigned = FCString::Atoi(*VersionString); if (ChunkVersionSigned >= 1) { // Increment by one so that the first patch file still gets more priority than the base pak file ChunkVersionNumber = (uint32)ChunkVersionSigned + 1; } } } } PakOrder += 100 * ChunkVersionNumber; }
挂载后,文件加载的访问次序由什么决定的
由 FPakEntry.ReadOrder(uint32) 决定
使用 < 的稳定排序算法
数值越大的,优先使用
数值相同的,先挂载的优先
ReadOrder = PakType
- 若文件名以
_p.pak
结尾,则根据 PAK 文件名规则提取 ChunkVersionSigned,即ReadOrder = + (100 + (ChunkVersionSigned >= 1?(uint32)ChunkVersionSigned + 1):1)
- 其中 ChunkVersion 默认值为1;
挂载细节
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
int32 FPakPlatformFile::MountAllPakFiles(const TArray<FString>& PakFolders, const FString& WildCard)
{
// ...
// Sort in descending order.
FoundPakFiles.Sort(TGreater<FString>());
// ...
for (int32 PakFileIndex = 0; PakFileIndex < FoundPakFiles.Num(); PakFileIndex++)
{
// ...
uint32 PakOrder = GetPakOrderFromPakFilePath(PakFilename);
// ...
for (int32 PakFileIndex = 0; PakFileIndex < FoundPakFiles.Num(); PakFileIndex++)
{
// ...
if (Mount(*PakFilename, PakOrder))
{
++NumPakFilesMounted;
}
}
}
}
int32 FPakPlatformFile::GetPakOrderFromPakFilePath(const FString& PakFilePath)
{
if (PakFilePath.StartsWith(FString::Printf(TEXT("%sPaks/%s-"), *FPaths::ProjectContentDir(), FApp::GetProjectName())))
{
return 4;
}
else if (PakFilePath.StartsWith(FPaths::ProjectContentDir()))
{
return 3;
}
else if (PakFilePath.StartsWith(FPaths::EngineContentDir()))
{
return 2;
}
else if (PakFilePath.StartsWith(FPaths::ProjectSavedDir()))
{
return 1;
}
return 0;
}
bool FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath /*= NULL*/, bool bLoadIndex /*= true*/)
{
// ...
PakFiles.StableSort();
// ...
}
MountPoint » 决定了业务访问路径
» 那么注意是否需要在PAK里有版本信息的标识,否则容易被hack回滚
可能可以在 Pak的文件头位置写入版本信息- 使用手动挂载 指定 pakorder的方式更符合设计
UE 默认情况下自动挂载的PAK路径
把打出来的 Pak 直接放到这三个目录下,在没有开启 Signing
的情况下,是会默认加载这三个路径下的所有 Pak 的。
1
2
3
4
5
# relative to Project Path
Content/Paks/
Saved/Paks/
# relative to Engine Path
Content/Paks
手动构建 Pak 文件
- 精细化控制 不同pak的压缩算法 以 优化包体、加载;
- 解耦 cook 与 package 环节独立,便于分布式构建
- 精准控制进包的资源
1
2
3
4
5
6
7
8
9
10
11
Output from:
C:\Users\junqiangzhu\Downloads\MyProject\MyProject.uproject
C:\Users\junqiangzhu\Downloads\MyProject\Saved\StagedBuilds\Android_ASTC\MyProject\Content\Paks\MyProject-Android_ASTC.pak
-create=E:\ug_UnrealEngine\Engine\Programs\AutomationTool\Saved\Logs\PakList_MyProject-Android_ASTC.txt
-cryptokeys=C:\Users\junqiangzhu\Downloads\MyProject\Saved\Cooked\Android_ASTC\MyProject\Metadata\Crypto.json
-secondaryOrder=C:\Users\junqiangzhu\Downloads\MyProject\Build\Android_ASTC\FileOpenOrder\CookerOpenOrder.log
-patchpaddingalign=0
-platform=Android
-compressionformats=Oodle -compressmethod=Kraken -compresslevel=4
-multiprocess
-abslog=E:\ug_UnrealEngine\Engine\Programs\AutomationTool\Saved\Logs\UnrealPak-MyProject-Android_ASTC-2022.07.14-10.45.11.txt
加载 加密的Pak文件
1
FCoreDelegates::GetPakEncryptionKeyDelegate().BindUObject(this, &UUGVersionMonitorStage::InitEncrypt);
1
2
3
4
5
6
7
8
9
10
11
12
13
void UUGVersionMonitorStage::InitEncrypt(uint8* Key)
{
FString KeyStr = TEXT("OqsU2kHaC38dsjmBbVssat3uNcwDAV07HzOla/lX24ifHU=");
TArray<uint8> KeyBase64Ary;
if (FBase64::Decode(KeyStr, KeyBase64Ary))
{
FMemory::Memcpy(TCHAR_TO_UTF8(*KeyStr), KeyBase64Ary.GetData(), FAES::FAESKey::KeySize);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("InitEncrypt Decode result invalid."));
}
}
怎么加载指定Pak中的文件?
打包时的mountPoint是怎么确定的
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
FString GetCommonRootPath(TArray<FPakInputPair>& FilesToAdd)
{
FString Root = GetLongestPath(FilesToAdd);
for (int32 FileIndex = 0; FileIndex < FilesToAdd.Num() && Root.Len(); FileIndex++)
{
FString Filename(FilesToAdd[FileIndex].Dest);
FString Path = FPaths::GetPath(Filename) + TEXT("/");
int32 CommonSeparatorIndex = -1;
int32 SeparatorIndex = Path.Find(TEXT("/"), ESearchCase::CaseSensitive);
while (SeparatorIndex >= 0)
{
if (FCString::Strnicmp(*Root, *Path, SeparatorIndex + 1) != 0)
{
break;
}
CommonSeparatorIndex = SeparatorIndex;
if (CommonSeparatorIndex + 1 < Path.Len())
{
SeparatorIndex = Path.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, CommonSeparatorIndex + 1);
}
else
{
break;
}
}
if ((CommonSeparatorIndex + 1) < Root.Len())
{
Root.MidInline(0, CommonSeparatorIndex + 1, false);
}
}
return Root;
}
其他API
PakPlatformFile->Unmount()
可卸载PakPlatformFile->GetMountedPakFilenames()
获取已加载的pak,可用于检测,避免重复加载
待验证
- PIE模式下,MountPoint 使用绝对路径
- 打包模式下,MountPoint 使用相对路径
加载代码例子
1
2
3
4
TSharedPtr<FPakPlatformFile> PakPlatformFile;
IPlatformFile* InnerPlatformFile;
UFUNCTION(BlueprintCallable)
bool LoadPak(const FString& PakPath);
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
void ALoadPakActor::BeginPlay()
{
Super::BeginPlay();
//获取当前使用的平台
InnerPlatformFile = &FPlatformFileManager::Get().GetPlatformFile();
UE_LOG(LogTemp, Warning, TEXT("InnerPlatformFile: %s"), InnerPlatformFile->GetName());
//初始化PakPlatformFile
PakPlatformFile = MakeShareable(new FPakPlatformFile());
PakPlatformFile.Get()->Initialize(InnerPlatformFile, TEXT(""));
}
bool ALoadPakActor::LoadPak(const FString& PakPath)
{
bool Result = false;
// 切换到 pak平台
FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile.Get());
// 获取pak文件
TSharedPtr<FPakFile> PakFile = MakeShareable(new FPakFile(InnerPlatformFile, *PakPath, false));
FString MountPoint = PakFile->GetMountPoint();
UE_LOG(LogTemp, Warning, TEXT("Default Mount Point: %s"), *MountPoint);
#if WITH_EDITOR
// PIE模式下,MountPoint 使用绝对路径
// 打包模式下,MountPoint 使用相对路径
MountPoint = FPaths::ConvertRelativePathToFull(MountPoint);
UE_LOG(LogTemp, Warning, TEXT("Default Mount Point Full Path: %s"), *MountPoint);
// 设置pak文件的Mount点,因为在制作pak的时候已在文本中设定 mount point,故省略此步骤
MountPoint = FPaths::ProjectContentDir() + TEXT("DLC/");
// 可在此处检测 默认MountPoint的绝对路径释放和本条语句执行结果是否一致
MountPoint = FPaths::ConvertRelativePathToFull(MountPoint);
PakFile->SetMountPoint(*MountPoint);
UE_LOG(LogTemp, Warning, TEXT("New Mount Point Full Path: %s"), *MountPoint);
#endif
// 对pak文件进行挂载
if (PakPlatformFile->Mount(*PakPath, 1, *MountPoint))
{
// 加载 pak 里的资源
UClass* BP_PakTestClass = LoadClass<AActor>(nullptr, TEXT("Blueprint'/Game/DLC/BP_PakTest1.BP_PakTest1_C'"));
if (BP_PakTestClass)
{
GetWorld()->SpawnActor<AActor>(BP_PakTestClass, FVector::ZeroVector, FRotator::ZeroRotator);
Result = true;
}
else
UE_LOG(LogTemp, Error, TEXT("Load BP_PakTest1 Class Failed"));
// 遍历 pak 里的资源
TArray<FString> AssetList;
PakFile->FindPrunedFilesAtPath(AssetList, *PakFile->GetMountPoint(), true, false, true);
for (FString itemPath : AssetList)
{
UE_LOG(LogTemp, Warning, TEXT("%-30s\t%s"), *FPackageName::GetShortName(itemPath), *itemPath);
// 此处可异步加载资源
}
}
else
UE_LOG(LogTemp, Error, TEXT("Mount Pak Failed"));
// 设置回原来的PlatformFile, UE4.26
// 不加该条语句,本测试崩溃,报错:Pure Virtual function being called while application was running
FPlatformFileManager::Get().SetPlatformFile(*InnerPlatformFile);
return Result;
}
- 使用 FCoreDelegates 挂载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool ALoadPakActor::LoadPak(const FString& PakPath)
{
bool Result = false;
if (FCoreDelegates::OnMountPak.IsBound())
{
if (FCoreDelegates::OnMountPak.Execute(PakPath, 0, nullptr))
{
UClass* BP_PakTestClass = LoadClass<AActor>(nullptr, TEXT("Blueprint'/Game/DLC/BP_PakTest1.BP_PakTest1_C'"));
if (BP_PakTestClass)
{
GetWorld()->SpawnActor<AActor>(BP_PakTestClass, FVector::ZeroVector, FRotator::ZeroRotator);
Result = true;
}
else
UE_LOG(LogTemp, Error, TEXT("Load BP_PakTest1 Class Failed"));
}
else
UE_LOG(LogTemp, Error, TEXT("OnMountPak.Execute() Failed"));
}
else
UE_LOG(LogTemp, Error, TEXT("OnMountPak.IsBound() Failed"));
return Result;
}
Cook产物一致性
- Shader Cache
- 序列化内容确定性
Shader变体的处理
Pak文件大小优化
FPakEntry -> Header 信息可以考虑精简
FPakFooterInfo 脚注信息可以选择精简
- 脚注的存在目的是什么?
Shader热更
当我们挂载热更的 pak 时,需要 pak order 大于基础包中的 pak
引擎启动时就已经自动加载了基础包中的 shaderbytecode,当程序运行起来之后挂载的 pak 中的 shaderbytecode 就不会被自动加载,这需要在挂载 pak 之后自己执行:
需要进一步验证,为什么?
1 2 3 4 5 6
void UFlibPatchParserHelper::ReloadShaderbytecode() { // 调用 FShaderCodeLibrary::OpenLibrary 函数即可。 FShaderCodeLibrary::OpenLibrary("Global", FPaths::ProjectContentDir()); FShaderCodeLibrary::OpenLibrary(FApp::GetProjectName(), FPaths::ProjectContentDir()); }
综上所述,我们热更 shader 时的流程如下:
- 执行 Cook 生成包含最新资源的 ushaderbytecode 文件
- 打包 ushaderbytecode 到 pak 中
- 手动加载 ushaderbytecode
重新生成 ushaderbytecode 可以直接使用以下 cook 命令:
1
UE4Editor-cmd.exe PROJECT_NAME.uproject -run=cook -targetplatform=WindowsNoEditor -Iterate -UnVersioned -Compressed
其实它会 rebuild metadata,AssetRegistry 之类的都会重新生成。执行完毕之后 Saved/Cooked
下的
AssetRegistry.bin
以及 Metadate
目录
/Content/ShaderArchive-*.ushaderbytecode
Enging/GlobalShaderCache*.bin
等文件都是生成之后最新的了。
这里知乎上也有人提到可以通过修改引擎源码来实现部分 Shader的编译,但是可能就需要支持 多
.ushaderbytecode
的情况了
Shader Patch
UE 中在 4.23 + 中开始提供了创建 ShaderPatch 的方法,需要提供 Old Metadata 和 New Metadata 的目录,Metadata
必须要具有以下目录结构:
1
2
3
4
5
6
7
8
9
10
11
D:\Unreal Projects\Blank425\Saved\Cooked\WindowsNoEditor\Blank425\Metadata>tree /a /f
卷 Windows 的文件夹 PATH 列表
卷序列号为 0C49-9EA3
C:.
| BulkDataInfo.ubulkmanifest
| CookedIniVersion.txt
| DevelopmentAssetRegistry.bin
|
\---ShaderLibrarySource
ShaderArchive-Global-PCD3D_SM5.ushaderbytecode
ShaderArchive-Blank425-PCD3D_SM5.ushaderbytecode
需要在打基础包时备份好当时的 Metadata 目录,把最新的工程在执行 Cook 之后的 Metadata 目录作为 New Metadata,基础包的作为 Old Metadata,调用引擎中的 FShaderCodeLibrary::CreatePatchLibrary
函数.
原理是,从 Old Metadata 序列化出旧的 Shader 数据,与 New Metadata 的做比对,有差异的部分作为 Patch 中的 Shader。
ShaderPatch 的更新不直接支持 Patch 的迭代
如:1.0 Metadata + 1.1 的 ShaderPatch,并不能生成 1.2 的 ShaderPatch,必须要基于 1.1 的完整 Metadata 才可以,即每次 Patch 必须要基于上一次完整的 Metadate 数据(Project 和 Global 的 ushaderbytecode 文件)
在工程管理上每次打包都需要把完整的 Metadata 收集起来。