UE5.7:StorageServerClient 接管机制导致的路径偏移与文件存在性误判

前言

在虚幻引擎项目中使用 Zen Store 进行数据存储与流送时,底层文件系统的行为模式会发生显著变化。当项目目录下存在特定标记文件时,引擎会激活 StorageServerClient 机制,重组Platform File Wrapper Chain。这种接管行为不仅会改变运行时的文件读写路径落点,还会影响文件存在性的判定逻辑。本文通过分析源码,记录该机制对 SandboxFile 挂载的影响以及由此引发的文件判定不一致问题。

问题和解决方案

问题场景

在构建流水线的时候,在尝试加入AS jit环节的时候,本地测试时发现AS_JITTED_CODE文件夹偶现没有产生在Cooked目录里。但是没有引起足够的重视,初步认为是BuildGraph写的有问题,也怀疑过是和Zen Storage有关系,为了快速先把pipeline搭建起来,选择了将ue.projectstore文件先转移之后进行jit代码生成和重编译。当进展到这一步之后就没有复现AS_JITTED_CODE文件夹生成到非预期位置的情况,随后又发现PrecompiledScript.Cache从打包机下载到本地执行的时候warning不匹配的问题,调试后惊人的发现在本地磁盘中删除了且确认完全不存在的情况下,判断PrecompiledScript.Cache文件存在性的逻辑依然是True,最终确认问题所在,

路径偏移问题

在启用 Zen Store 的环境下,原本预期生成在 Saved/Cooked/[Platform]/Sandbox 目录下的缓存文件(如 AngelScript 预编译代码或 JIT 缓存)会直接出现在项目根目录。

其根本原因在于 StorageServerClient 的优先级高于 SandboxFile。当引擎检测到 ue.projectstore 文件后,会直接将 StorageServerClient 作为网络平台文件挂载,从而跳过了 SandboxFile 的创建流程。由于缺失了 Sandbox 的路径重定向映射,所有使用 FPaths::ProjectDir()FPaths::RootDir() 等接口生成的路径均回归至物理磁盘真实路径,造成偏移现象。

解决方案

  1. 启动项目时添加命令行参数 -SkipZenStore 强制禁用接管逻辑,恢复 Sandbox 挂载。
  2. 将受影响的缓存生成路径移出 Cooked 目录体系,改用 Intermediate 等不被 Zen 扫描的目录。

文件存在性误判

手动删除磁盘上的缓存文件后,调用 IPlatformFile::FileExists 依然返回 true,导致逻辑错误。

这是由于 Zen 模式下的文件存在性判定优先查询服务器端的 TOC 指令。在 Cook 阶段,Cooked/[Platform] 下的非排除文件会被写入 zenfs.manifest 清单。运行时,FStorageServerPlatformFile 会根据该清单填充本地 ServerToc。只要清单中存在记录,即使物理文件已被删除,底层判定仍会维持原有的存在性状态。

解决方案

  1. 在 Cook 阶段排除特定的缓存扩展名,修改 ZenFileSystemManifest.cpp 中的过滤规则。
  2. 确保在执行 Cook 任务前清理 Cooked 目录,避免残留的旧缓存被编入清单。

相关源代码

ue.projectstore 的查找与触发点

文件F:\UE_Release\Engine\Source\Runtime\StorageServerClient\Private\StorageServerPlatformFile.cpp

关键函数:FStorageServerPlatformFile::TryFindProjectStoreMarkerFile

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
TArray<FString> PotentialProjectStorePaths;
if (CustomProjectStorePath.IsEmpty())
{
FString RelativeStagedPath = TEXT("../../../");
FString RootPath = FPaths::RootDir();
FString PlatformName = GetCookedPlatformName();
FString CookedOutputPath = FPaths::Combine(FPaths::ProjectDir(), TEXT("Saved"), TEXT("Cooked"), PlatformName);

PotentialProjectStorePaths.Add(RelativeStagedPath);
PotentialProjectStorePaths.Add(CookedOutputPath);
PotentialProjectStorePaths.Add(RootPath);
}
else
{
PotentialProjectStorePaths.Add(CustomProjectStorePath);
}

for (const FString& ProjectStorePath : PotentialProjectStorePaths)
{
FString ProjectMarkerPath = ProjectStorePath / TEXT("ue.projectstore");
if (IFileHandle* ProjectStoreMarkerHandle = Inner->OpenRead(*ProjectMarkerPath); ProjectStoreMarkerHandle != nullptr)
{
UE_LOG(LogStorageServerPlatformFile, Display, TEXT("Found '%s'"), *ProjectMarkerPath);
return TUniquePtr<FArchive>(new FArchiveFileReaderGeneric(ProjectStoreMarkerHandle, *ProjectMarkerPath, ProjectStoreMarkerHandle->Size()));
}
}

含义:当 cooked 目录下(或 root/相对路径)存在 ue.projectstore,后续 ShouldBeUsed() 就可能启用 StorageServerClient。

挂载冲突

文件F:\UE_Release\Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp

LaunchCheckForFileOverride 中明确说明:

1
2
// NetworkPlatformFile can be only one of StorageServerClient, StreamingFile or NetworkFile
// Having a NetworkPlatformFile present prevents creation of Pakfile, CachedReadFile and SandboxFile

并且代码体现:

1
2
3
4
5
6
7
8
9
NetworkPlatformFile = ConditionallyCreateFileWrapper(TEXT("StorageServerClient"), CurrentPlatformFile, CmdLine);
...
if (!NetworkPlatformFile)
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("CachedReadFile"), CurrentPlatformFile, CmdLine);
...
PlatformFile = ConditionallyCreateFileWrapper(TEXT("SandboxFile"), CurrentPlatformFile, CmdLine);
...
}

含义:

  • 一旦 StorageServerClient 成为 NetworkPlatformFileSandboxFile 不会被创建。
  • 这会导致写入目录 不再被 sandbox 重定向

Sandbox 的路径还原示例:

1
2
3
4
5
6
7
8
if (FullSandboxPath.StartsWith(SandboxGameDirectory))
{
OriginalPath = FullSandboxPath.Replace(*SandboxGameDirectory, *FPaths::ProjectDir());
}
else if (FullSandboxPath.StartsWith(SandboxRootDirectory))
{
OriginalPath = FullSandboxPath.Replace(*SandboxRootDirectory, *FPaths::RootDir());
}

Sandbox 是通过 wrapper 做路径映射;它 不会改变 FPaths::RootDir(),但会改变实际读写路径的落点。

Zen 文件清单生成逻辑

文件: F:\UE_Release\Engine\Source\Developer\IoStoreUtilities\Private\ZenFileSystemManifest.cpp

关键函数
FZenFileSystemManifest::Generate 里会扫描 Cooked 输出目录并生成清单:

1
2
3
4
5
6
7
8
9
FFileFilter CookedFilter = FFileFilter()
.ExcludeDirectoryLeafName(TEXT("Metadata"))
.ExcludeExtension(TEXT("uasset"))
.ExcludeExtension(TEXT("ubulk"))
.ExcludeExtension(TEXT("uexp"))
.ExcludeExtension(TEXT("umap"))
.ExcludeExtension(TEXT("uregs"));
Generator.AddFilesFromDirectory(TEXT("/{engine}"), Generator.GetCookedEngineDir(), true, &CookedFilter);
Generator.AddFilesFromDirectory(TEXT("/{project}"), Generator.GetCookedProjectDir(), true, &CookedFilter);

Cooked/<Platform> 下的所有文件(除了明确排除的扩展)都会进入 Zen 文件清单。

运行时 FileExists 优先查 Zen 端 TOC

文件: F:\UE_Release\Engine\Source\Runtime\StorageServerClient\Private\StorageServerPlatformFile.cpp

关键函数
FStorageServerPlatformFile::FileExists

1
2
3
4
5
6
7
8
9
10
bool FStorageServerPlatformFile::FileExists(const TCHAR* Filename)
{
TStringBuilder<1024> StorageServerFilename;
if (MakeStorageServerPath(Filename, StorageServerFilename) && ServerToc.FileExists(*StorageServerFilename))
{
return true;
}

return (LowerLevel && IsNonServerFilenameAllowed(Filename)) ? LowerLevel->FileExists(Filename) : false;
}

只要 ServerToc(Zen 文件列表)里有记录,就直接返回 true。ServerToc 的内容来自 Zen 服务器上的文件清单,与本地磁盘是否存在无关

TOC来自 Zen 服务器

文件: F:\UE_Release\Engine\Source\Runtime\StorageServerClient\Private\StorageServerPlatformFile.cpp

SendGetFileListMessage() 会向 Zen 请求文件清单并填充 TOC:

1
2
3
4
5
6
7
8
9
bool FStorageServerPlatformFile::SendGetFileListMessage()
{
Connection->FileManifestRequest([&](FIoChunkId Id, FStringView Path, int64 RawSize)
{
...
ServerToc.AddFile(Id, Path, RawSize);
...
});
}

运行时的 ServerToc 由 Zen 端清单驱动。

-SkipZenStore 参数与启用条件

文件F:\UE_Release\Engine\Source\Runtime\StorageServerClient\Private\StorageServerPlatformFile.cpp

关键位置在 ShouldBeUsed 的开头:

1
2
if (FParse::Param(FCommandLine::Get(), TEXT("SkipZenStore")))
return false;

启动参数 -SkipZenStore 会直接让 StorageServerClientShouldBeUsed() 返回 false。这意味着 即使 ue.projectstore 存在,也不会接管平台文件链,平台文件链会继续走本地文件系统(Pak/CachedRead/Sandbox 等照常挂载)。

同一函数后续会尝试读取 ue.projectstore

1
2
3
4
5
6
TUniquePtr<FArchive> ProjectStoreMarkerReader = TryFindProjectStoreMarkerFile(Inner);
if (ProjectStoreMarkerReader != nullptr)
{
...
UE_LOG(LogStorageServerPlatformFile, Display, TEXT("Using connection settings from ue.projectstore: ..."));
}

总结和建议

  1. ZenFileSystem会扫描 Cooked 输出目录并生成清单
  2. ue.projectstore存在会触发StorageServerClient的启用
  3. ue.projectstore的存在性检查的范围比预计要广,甚至会检查../../../
  4. 可以通过-SkipZenStore避免pfc被接管

当然,如果一开始没有选择将workspace改为incremental就不会遇到这个问题(当然也有别的代价)。pipeline还是应该做好清理工作……

01a009508d1f628bb2dddc1a94626b87.png


UE5.7:StorageServerClient 接管机制导致的路径偏移与文件存在性误判
http://muchenhen.com/posts/25723/
作者
木尘痕
发布于
2026年1月31日
许可协议