文件系统、VPK 与资源缓存
运行时资源(地图、模型、材质、脚本、配置)基本都通过 IFileSystem 按“搜索路径 + 路径 ID”读取;VPK 包与地图内嵌包会参与同一套查找。读取到的大块数据(模型等)再由 datacache 以 LRU 方式缓存,并在换图时锁定/释放。
相关源码文件#
filesystem/basefilesystem.cpp、filesystem/filesystem_stdio.cpp、filesystem/QueuedLoader.cppvpklib/packedstore.cppdatacache/datacache.cpp、datacache/mdlcache.cpp
1. 搜索路径与路径 ID#
- 启动时,启动器设置 Steam 环境并从 gameinfo 挂载内容,可通过
-vpk追加 VPK,并加入platform搜索路径:launcher/launcher.cpp:895-936。专用服务器在PreInit中做同样的事,并额外调用FileSystem_LoadSearchPaths:dedicated/sys_ded.cpp:341-369。 AddSearchPathInternal:.bsp路径交给地图包处理;其它路径规范化为绝对路径(Windows 下转小写);同一路径+路径 ID 已存在时,PATH_ADD_TO_TAIL直接忽略,PATH_ADD_TO_HEAD则移到表头;可同时加入该目录下的包文件:filesystem/basefilesystem.cpp:2671-2800。- 文件名可以用
//pathid/relative/path形式内嵌路径 ID;//*/表示不限路径 ID;与参数中的路径 ID 冲突时告警:filesystem/basefilesystem.cpp:4314-4360。 - 游戏 DLL 从
GAMEBIN路径 ID 加载:engine/sys_dll2.cpp:2191-2193、filesystem/basefilesystem.cpp:7246-7262。
2. 打开一个文件#
OpenForRead → FindFileInSearchPaths:按搜索路径顺序逐个调用 FindFile,第一个成功的返回(主机平台默认只查包内文件):filesystem/basefilesystem.cpp:4266-4270、filesystem/basefilesystem.cpp:4209-4261。
FindFile 对相对路径的处理顺序:先在所有已挂载的 VPK 中查找,命中则直接返回;然后如果该搜索路径是包文件就从包中读,否则拼出绝对路径读磁盘:filesystem/basefilesystem.cpp:4167-4204。绝对路径还支持 xxx.zip/内部路径 形式:filesystem/basefilesystem.cpp:4151-4166。
VPK 优先于松散文件(静态闭环,未运行验证)。 FindFileInSearchPaths 按搜索路径逐个调用 FindFile,第一个返回非空句柄者胜出:filesystem/basefilesystem.cpp:4252-4260。而 FindFile 的 VPK 循环遍历的是文件系统级的全局 m_VPKFiles 列表,循环体完全不使用当前搜索路径 path:filesystem/basefilesystem.cpp:4171-4189。因此只要请求的 path ID 至少匹配一个搜索路径,第一次迭代就会先查遍所有已挂载 VPK;同名文件在 VPK 中的版本优先于任何松散文件,而且 VPK 命中不受 path ID 约束。SUPPORT_VPK 在 Windows(非 X360/PS3/Linux 分支)与 POSIX(非 PS3)上都被定义:filesystem/basefilesystem.h:25-31、filesystem/basefilesystem.h:37-48。VPK 列表之间的先后由 AddVPKFile 的 PATH_ADD_TO_TAIL/HEAD 决定(见下)。
AddVPKFile 要求扩展名为 .vpk(否则告警忽略),按绝对路径去重,用 CPackedStore 打开,为空则丢弃,按 PATH_ADD_TO_TAIL/HEAD 加入列表:filesystem/basefilesystem.cpp:1295-1340。
纯净服务器(sv_pure)依赖文件系统的白名单跟踪,专用服务器在 ModInit 中开启:engine/sys_dll2.cpp:2837-2840。
异步读取与排队加载器#
- 异步 I/O 用一个专用线程池实现:
InitAsync在命令行没有-noasync时创建名为FsAsyncIO的池;PC 上线程数取逻辑核数与 3 的较小值(至少 1),栈 256 KB;主机平台改为单 I/O 线程:filesystem/basefilesystemasync.cpp:668-747。 AsyncReadMultiple*为每个请求创建CFileAsyncReadJob(nBytes < 0时创建只取文件大小的作业)。非异步模式、请求带FSASYNC_FLAGS_SYNC、或线程池不存在时,在调用线程同步执行;否则投入线程池。调用方可拿到作业句柄用于AsyncFinish等待,否则立即释放引用:filesystem/basefilesystemasync.cpp:773-821。挂起、恢复、等待、冲刷的入口:filesystem/basefilesystemasync.cpp:969、filesystem/basefilesystemasync.cpp:983、filesystem/basefilesystemasync.cpp:1027、filesystem/basefilesystemasync.cpp:1115。CQueuedLoader(换图时批量预读资源的排队加载器)在 PC 上不生效:BeginMapLoading在IsPC()时直接返回false,命令行-noqueuedload/-noqueuedloader也会关闭它:filesystem/QueuedLoader.cpp:1801-1812。CMDLCache中依赖它的分支(例如PreloadModel)也先检查IsGameConsole():datacache/mdlcache.cpp:1881、datacache/mdlcache.cpp:4548-4558。因此 PC 版 CS:GO 的换图加载走普通的同步/异步文件读取(基于源码条件判断)。
3. 通用数据缓存 datacache#
CDataCacheSection 是带预算的 LRU 缓存:
| 操作 | 行为 | 证据 |
|---|---|---|
Add/AddEx | 加入条目,超预算时清除旧条目;已存在返回 false | datacache/datacache.cpp:186-196 |
Lock/Unlock | 引用计数锁定,锁定的条目不会被清除 | datacache/datacache.cpp:398-430 |
Get | 可选“帧锁定”(本帧内保证有效) | datacache/datacache.cpp:475-489 |
Purge | 从最旧的未锁定条目开始丢弃,直到释放足够字节 | datacache/datacache.cpp:740-770 |
4. 模型缓存 CMDLCache#
FindMDL按相对路径登记模型句柄:datacache/mdlcache.cpp:1547;GetStudioHdr按需加载模型头(见 模型、动画与粒子):datacache/mdlcache.cpp:2935-2990。- 每次进入加载类 HostState 时
BeginMapLoad:打破帧锁,并释放上一张地图强制锁定的模型头与顶点数据:engine/host_state.cpp:786-816、datacache/mdlcache.cpp:3369-3407。EndMapLoad完成挂起的加载,并在启用锁定时清理加载期间未被引用的模型:datacache/mdlcache.cpp:3412-3420。 - 引擎主循环用
MDLCACHE_COARSE_LOCK包住整帧,并每帧MarkFrame:engine/host.cpp:3986、engine/host.cpp:4098;服务器 tick 与游戏帧内大量使用MDLCACHE_CRITICAL_SECTION:engine/host.cpp:3341、game/server/gameinterface.cpp:1466。
约束与边界#
- 仓库中没有任何
.vpk/.bsp/.mdl/.vtf等内容文件;文件系统与缓存的行为只能静态分析。 - 异步 I/O 只说明了线程池与作业投递;作业优先级、
AsyncRead的内存分配回调、Steam 文件系统、Xbox 安装器与文件 CRC 跟踪的细节未展开。