CD Code Digest Hub /CS:GO

文件系统、VPK 与资源缓存

10 分钟阅读 6 个章节

运行时资源(地图、模型、材质、脚本、配置)基本都通过 IFileSystem 按“搜索路径 + 路径 ID”读取;VPK 包与地图内嵌包会参与同一套查找。读取到的大块数据(模型等)再由 datacache 以 LRU 方式缓存,并在换图时锁定/释放。

相关页面:客户端启动链 · 地图加载 · 模型、动画与粒子

相关源码文件#

  • filesystem/basefilesystem.cpp、filesystem/filesystem_stdio.cpp、filesystem/QueuedLoader.cpp
  • vpklib/packedstore.cpp
  • datacache/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加入条目,超预算时清除旧条目;已存在返回 falsedatacache/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 跟踪的细节未展开。