CD Code Digest Hub /GTA V · v2

paging 与资源:pgBase、datResource 重定位、资源构建与流式读取

39 分钟阅读 10 个章节 1 张图

RAGE 的“资源”(resource)是离线把整棵 C++ 对象图按目标平台内存布局序列化成一块或多块内存镜像的格式:运行时只需把这些块读入内存、按块表调整指针(fixup)、再用 placement new 调用每个对象的“资源构造函数”恢复 vtable 与运行时状态,无需逐字段反序列化。所有 .ydr/.ytd/.yft/.ybn/.ycd/.ymt/...(Win64;其他平台首字母不同)都是这种文件。本页说明资源文件头与块布局、datResource 的修复算法、pgBase 及其页表、pgRscBuilder 的构建与加载入口、pgStreamer 异步读取,以及 pgDictionary/pgRef/datOwner 等资源内指针类型。

背景与相关页面#

相关源码文件#

区域文件(相对 src/dev_ng/rage/base/src/)
资源头 / 块描述data/resourceheader.h/.cpp
重定位data/resource.h/.cpp、paging/base.cpp(datResource 构造与 GetFixup)
资源内指针与结构声明data/struct.h(datRef/datOwner/datTypeStruct/datSwapper)、paging/ref.h、paging/owner.h、paging/handle.h
资源基类paging/base.h/.cpp、paging/base_spu.h、paging/knownrefpool_config.h
构建与加载paging/rscbuilder.h/.cpp
流式读取paging/streamer.h/.cpp、paging/streamer_internal.h
字典paging/dictionary.h
扩展名表rage/framework/src/fwsys/fileExts.h

1. 资源文件格式#

1.1 文件头#

datResourceFileHeader 是 16 字节:u32 Magic = 0x37435352 ('RSC7')、int Version(应用定义;打包进 RPF 后只保留低 8 位)、datResourceInfo Info(8 字节,虚拟/物理两段的尺寸编码)。资源在构建时被安置在固定的“源地址空间”:虚拟段基址 0x50000000,物理段基址 0x60000000 src/dev_ng/rage/base/src/data/resourceheader.h:117-130。 IsValidResource 在 Win32 PC(工具侧)还接受字节交换后的魔数并就地翻转头部 src/dev_ng/rage/base/src/data/resource.cpp:108-127。

1.2 datResourceInfo:用 32 位描述一段的块序列#

每段(Virtual、Physical)用一个 32 位位域 Sizes 描述:LeafShift(4 位,基本块 = 叶尺寸 << LeafShift)、Head16Count(1)、Head8Count(2)、Head4Count(4)、Head2Count(6)、BaseCount(7)、HasTail2/4/8/16(各 1)、Version(4)。即一段内存由若干 16×/8×/4×/2×/1× 基本块加至多四个 1/2…1/16 尾块组成;8 位资源版本被拆成两段各 4 位 src/dev_ng/rage/base/src/data/resourceheader.h:47-113。

叶尺寸(leaf size):PS3 虚拟 4096 / 物理 5504,其它平台均为 8192;资源编译器按目标平台动态设置 src/dev_ng/rage/base/src/data/resourceheader.h:31-45、src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1152-1164。

datResourceInfo::GenerateMap 依次为虚拟段、物理段展开块表:从最大块开始,每类块按数量重复,块尺寸逐级减半;源地址从固定基址连续递增,目标地址待分配 src/dev_ng/rage/base/src/data/resourceheader.cpp:14-61。单次读取的最大块数 datResourceChunk::MAX_CHUNKS = 128 src/dev_ng/rage/base/src/data/resourceheader.h:133-143。

推断:这种“2 的幂块 + 尾块”设计使运行时分配可以直接映射到 buddy 分配器的节点尺寸(pgRscBuilder::ComputeLeafSize 在 ENABLE_BUDDY_ALLOCATOR 时调用 sysMemBuddyAllocator::ComputeNodeSize,src/dev_ng/rage/base/src/paging/rscbuilder.cpp:450-466),从而避免大块连续内存需求。

1.3 磁盘布局#

SaveBuild 写出:16 字节头(大小、魔数、版本按目标平台字节序交换)+ 虚拟段与物理段连续做 raw deflate(deflateInit2(..., -MAX_WBITS, ...);64 位平台总是 zlib,32 位 Xenon 默认用 XCompress,-fastCompression 切换)后的数据;-uncompressedresources 时写未压缩数据。可选 AES 加密(新平台存在按文件选择密钥的机制;本页不记录算法与密钥)。版本号低 8 位不得为 0 src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1306-1500。放入 RPF 后,魔数不再保存,datResourceInfo 与版本单独记录在包条目中(见 file-system-and-rpf.md),这与头文件注释“not kept in an archive”一致 src/dev_ng/rage/base/src/data/resourceheader.h:123-126。

1.4 扩展名#

扩展名首字母是平台码:代码里以 '#' 占位,由 pgRscBuilder::ConstructName 替换为 g_sysPlatform(路径中的 # 也会被替换) src/dev_ng/rage/base/src/paging/rscbuilder.cpp:75-110。framework 中定义了各类型的模式与每平台扩展名,例如 Win64 ydr/ytd/ymt、Durango ddr/dtd/dmt、Orbis odr/otd/omt、Xenon x??、PS3 c??、平台无关 idr/itd src/dev_ng/rage/framework/src/fwsys/fileExts.h:79-271、src/dev_ng/rage/framework/src/fwsys/fileExts.h:293-328。

模式类型版本常量
#dr / #dd可绘制体 / 可绘制体字典rmcDrawable::RORC_VERSION + 18
#ft碎片(fragType)rvFragObject
#td纹理字典grcTexture*::RORC_VERSION
#cd动画剪辑字典crClipDictionary::RORC_VERSION
#bn / #bd物理包围体 / 包围体字典phBound::RORC_VERSION
#ld布料phVerletCloth::RORC_VERSION
#pt粒子特效列表ptxFxList::RORC_VERSION
#mt / #map / #typ元数据(PSO)/ 地图数据 / 地图类型—
#nv / #sc / #nd导航网格 / 脚本 / 路网节点—

证据:src/dev_ng/rage/framework/src/fwsys/fileExts.h:30-43、src/dev_ng/rage/framework/src/fwsys/fileExts.h:294-320。


2. datResource:运行时指针修复#

2.1 datResourceMap#

加载时的块表:VirtualCount/PhysicalCount/RootVirtualChunk、VirtualBase(根对象所在块的目标地址)、Chunks[128](每项 SrcAddr/DestAddr/Size,虚拟块在前)以及“上次命中”缓存 src/dev_ng/rage/base/src/data/resource.h:42-108。 Xenon/PS3 的 RESOURCE_HEADER 模式下还区分 RPF/字典/脚本头类型,可把小 RPF 头放入专用 MEMTYPE_HEADER_VIRTUAL 堆 src/dev_ng/rage/base/src/data/resource.h:38-40、src/dev_ng/rage/base/src/data/resourceheader.h:13-29。

2.2 修复栈与算法#

  • datResource(map, debugName, isDefrag) 构造时压入 TLS 栈 datResource_sm_Current(注释称为规避 Sony TLS 问题而用全局 TLS 变量而非类静态),析构时弹出;构造期间 ++g_DisableInitNan 避免资源对象被 NaN 填充 src/dev_ng/rage/base/src/data/resource.h:21-28、src/dev_ng/rage/base/src/paging/base.cpp:2132-2191。
  • 默认 BINARY_SEARCH_FIXUPS:构造时把块的源区间与目标区间分别排序;GetFixup(ptr) 在源区间二分查找所属块,返回 DestAddr - SrcAddr;找不到则 Quitf(ERR_SYS_INVALIDRESOURCE_5, "Invalid fixup ...")。ContainsThisAddress 在目标区间二分。Xenon/PS3 在已知引用开启时另有表查找加速(TABLE_SEARCH_FIXUPS,仅更新线程) src/dev_ng/rage/base/src/data/resource.h:175-187、src/dev_ng/rage/base/src/paging/base.cpp:2196-2260。
  • 静态模板 datResource::Fixup(ptr):若当前有修复且指针字段本身位于当前资源内,则修复;否则(非资源上下文)把指针置 NULL。datResource::Place(ptr, ...) 修复后再调用 T::Place(newPtr, rsc, ...) 递归构造被拥有对象 src/dev_ng/rage/base/src/data/resource.h:267-318。
  • SPU 版只支持单一连续容器、常量偏移 src/dev_ng/rage/base/src/data/resource.h:110-171。

2.3 资源构造约定#

每个可资源化类型提供“资源构造函数” T(datResource&) 与静态 Place:DECLARE_PLACE(T) 声明、IMPLEMENT_PLACE(T) 实现为 ::new (that) T(rsc);不需要修复的简单类用 DECLARE_DUMMY_PLACE;需要按枚举创建不同子类的虚工厂类自写 Place。__TOOL 构建下 IMPLEMENT_PLACE 为空实现 src/dev_ng/rage/base/src/data/resource.h:333-420。

资源内的指针字段使用包装类型,使资源构造函数在成员初始化阶段自动完成修复:

类型语义资源构造行为证据
datRef<T>非拥有引用datResource::Fixup(ptr)src/dev_ng/rage/base/src/data/struct.h:110-121
datOwner<T>拥有指针datResource::Place(ptr[, p1, p2]),递归放置子对象src/dev_ng/rage/base/src/data/struct.h:124-133
pgRef<T>非拥有引用(可参与已知引用跟踪)有当前资源则 PointerFixup,否则置 0src/dev_ng/rage/base/src/paging/ref.h:17-60
pgOwner<T>拥有指针(可碎片整理版本)datResource::Place(ptr)src/dev_ng/rage/base/src/paging/owner.h:15-60
pgHandle<T>经句柄表的双重间接仅在 ENABLE_DEFRAGMENTATION 时有意义src/dev_ng/rage/base/src/paging/handle.h:46-70

atArray 等容器也有资源构造函数(见 math-and-containers.md)。DECLARE_PADDED_POINTER 当前恒为普通指针(32 位填充分支被 && 0 关闭) src/dev_ng/rage/base/src/data/struct.h:15-23。

2.4 DeclareStruct 与字节交换(构建期)#

__DECLARESTRUCT = !__FINAL && !__SPU && (!__PPU || __DEV) && !RSG_ORBIS。资源编译器为每个类型实现 DeclareStruct(datTypeStruct&),用 STRUCT_FIELD 列出字段;datSwapper 系列在 g_ByteSwap(仅资源编译器中为变量)为真时交换字节序,指针字段先 datRelocatePointer(把构建堆地址改写为 0x5…/0x6… 源地址)再交换 src/dev_ng/rage/base/src/data/struct.h:43-106。这就是“跨平台构建”的核心:PC 上的资源编译器以目标平台的字节序、指针宽度与叶尺寸生成镜像(平台判定见 system-and-diagnostics.md 的 g_sysPlatform)。


3. pgBase:可分页对象基类#

3.1 结构#

pgBase 只有一个虚析构(Destroy())与一个数据成员 pgBaseNode* m_FirstNode。对顶层资源对象,m_FirstNode 指向资源内嵌的 pgBasePageMap(VirtualCount/PhysicalCount/RootVirtualChunk/Subtype + 每块一个 size_t PageInfo[]),其后可链接若干 pgBaseMetaData(按 Type 在 pgBaseMetaDataType 注册表中查找放置函数) src/dev_ng/rage/base/src/paging/base.h:155-369、src/dev_ng/rage/base/src/paging/base_spu.h:18-74。构建期 ReadyBuild 首先在虚拟堆中分配最坏情况大小的页表、挂到顶层对象 m_FirstNode,再反复打包块(虚拟最多占一半的 128 个桶,物理用剩余)直到页表尺寸稳定 src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1216-1275。

3.2 生命周期#

图证据:PlaceStream / Load 模板 src/dev_ng/rage/base/src/paging/rscbuilder.h:416-430、src/dev_ng/rage/base/src/paging/rscbuilder.h:533-543;pgBase::pgBase src/dev_ng/rage/base/src/paging/base.cpp:246-265;Destroy/FreeMemory src/dev_ng/rage/base/src/paging/base.cpp:322-412;MakeDefragmentable/RegenerateMap src/dev_ng/rage/base/src/paging/base.cpp:442-520。

要点:

  • 页表压缩:PageInfo[i] = DestAddr | log2(Size/leafSize),要求块地址低位为 0、log2 < 16(用 TrapGE/TrapNZ 而不是 Assert,Release 中也生效) src/dev_ng/rage/base/src/paging/base.cpp:468-480。
  • 释放:若资源虚拟分配器 SupportsAllocateMap(),整张表一次 FreeMap;否则逐块释放虚拟块(非 Final 下先以 0xEE 填充)与物理块;USE_SPARSE_MEMORY 时根块最后用 DeferredFree 释放 src/dev_ng/rage/base/src/paging/base.cpp:342-412。
  • Orbis:放置时把物理块内存类型改为 CPU/GPU 只读、Garlic 总线写回 src/dev_ng/rage/base/src/paging/base.cpp:254-262。
  • Validate(userData)(非 Final)检查每块仍为有效分配且 user data 匹配,用于定位过早释放 src/dev_ng/rage/base/src/paging/base.cpp:414-440。
  • sm_ThreadedPlace(DECLARE_THREADED_PLACE())声明该类型允许在流式线程上放置 src/dev_ng/rage/base/src/paging/base.h:41-43、src/dev_ng/rage/base/src/paging/base.h:161-162。

3.3 已知引用与碎片整理:在本代码中关闭#

paging/knownrefpool_config.h 中 ENABLE_KNOWN_REFERENCES 0、ENABLE_DEFRAGMENTATION (ENABLE_KNOWN_REFERENCES && 1) → 两者均为 0 src/dev_ng/rage/base/src/paging/knownrefpool_config.h:1-2。因此 ADD_KNOWN_REF/REMOVE_KNOWN_REF/UPDATE_KNOWN_REF 退化为空或普通赋值 src/dev_ng/rage/base/src/paging/base.h:126-144,pgHandle 双重间接与 PatchAllReferences 碎片整理路径不生效,pgBase 构造走 !ENABLE_KNOWN_REFERENCES 分支(仅修复 m_FirstNode 并填页表) src/dev_ng/rage/base/src/paging/base.cpp:248-265。源码中 datResource_IsDefragmentation 在此配置下为常量 false src/dev_ng/rage/base/src/data/resource.h:24-28。

3.4 派生基类#

类用途证据
pgBaseRefCounted原子引用计数,Release() 归零即 delete thissrc/dev_ng/rage/base/src/paging/base.h:373-411
pgBaseAllocated非资源对象自带 3 个 u32 页表存储src/dev_ng/rage/base/src/paging/base.h:413-420
pgBasePlatformNeutral32/64 位布局一致(32 位时补 8 字节),放置前 BeforePlace(that, originatingPlatform) 处理来源平台差异;psoResourceData 继承自它src/dev_ng/rage/base/src/paging/base.h:422-445
pgArray<T>资源化数组基类src/dev_ng/rage/base/src/paging/array.h:20

4. pgRscBuilder:构建与加载入口#

4.1 构建(__RESOURCECOMPILER)#

  1. BeginBuild(secondPass, heapSize, ...):按目标平台配置叶尺寸,创建虚拟/物理两个 singlePassAllocator 并组合成 rscAllocator,替换当前线程的 Current/Container 分配器——此后所有 new 都落入资源堆(ONE_PASS_BUILD = 1) src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1166-1214、src/dev_ng/rage/base/src/paging/rscbuilder.h:23、src/dev_ng/rage/base/src/paging/rscbuilder.h:104-145。
  2. 构造对象图(顶层对象必须是 BeginBuild 后第一个分配)。
  3. ReadyBuild(top):分配页表、打包桶、给所有分配分配最终(源)地址、重定位顶层指针。
  4. 调用 DeclareStruct(字节交换/指针重定位)。
  5. SaveBuild(name, "#xx", version, ...):复制堆、压缩、写头。
  6. EndBuild(top):恢复分配器,删除构建堆 src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1277-1295。

4.2 加载#

入口用途证据
Load<T>(t, name, ext, version[, u, v, w])同步:LoadBuild(内部 BeginStream+阻塞 EndStream)→ datResource → T::Place → PostPlacesrc/dev_ng/rage/base/src/paging/rscbuilder.h:416-489、src/dev_ng/rage/base/src/paging/rscbuilder.cpp:413-448
BeginStream/OpenStream/ContinueStream/EndStream/CancelStream异步三段式src/dev_ng/rage/base/src/paging/rscbuilder.h:239-380
PlaceStream<T>(t, info, map, debugName[, ...])数据已由流式系统读入 map 后只做放置;被 framework/游戏大量使用(导航网格、路网、录像、音频几何等)src/dev_ng/rage/base/src/paging/rscbuilder.h:533-570;调用例:src/dev_ng/rage/framework/src/ai/navmesh/navmesh.cpp:395、src/dev_ng/game/vehicleAi/pathfind.cpp:344
AllocateMap(map)若分配器支持则整表分配;否则逐块分配虚拟/物理,失败回滚并返回 falsesrc/dev_ng/rage/base/src/paging/rscbuilder.cpp:315-395
ValidateMap(name, map, hdr)GenerateMap + AllocateMap,带内存跟踪标签src/dev_ng/rage/base/src/paging/rscbuilder.cpp:397-410

5. pgStreamer:异步读取与解压#

pgStreamer 是 RPF 之上的底层异步读服务;framework 的 strStreamingLoader 通过 pgStreamer::Read 发出请求(src/dev_ng/rage/framework/src/streaming/streamingloader.cpp:2102)。

  • 句柄:Open(filename, &size, resourceVersion) 解析路径到 RPF 设备(PC 或 ENABLE_RAW_STREAMER 时非 RPF 文件走 pgRawStreamer),句柄为 fiCollection::MakeHandle(packfileIndex, entryIndex);若给定 resourceVersion,则比较包条目中的版本与 resourceVersion & 255,不符则报错/断言并返回 Error src/dev_ng/rage/base/src/paging/streamer.cpp:2074-2117。
  • 设备与线程:Device { OPTICAL, HARDDRIVE }(DUAL_QUEUE_SUPPORT=1);每个设备一个 pgStreamerDeviceHandler,含一个 zlibInflater、一个读线程 pgReadData("[RAGE] DVD/HDD Reader")、一个处理线程("[RAGE] DVD/HDD Streamer"),以及 CRITICAL_QUEUE / NORMAL_QUEUE 两条请求队列 src/dev_ng/rage/base/src/paging/streamer.h:76-80、src/dev_ng/rage/base/src/paging/streamer.cpp:659-697。
  • 容量:同时在途请求上限 MaxRequests = 32(位图池大小);读缓冲每块 MAX_READ = 512 KB,光驱 3 块、硬盘 24 块 src/dev_ng/rage/base/src/paging/streamer.cpp:661-668、src/dev_ng/rage/base/src/paging/streamer_internal.h:338-342、src/dev_ng/rage/base/src/paging/streamer_internal.h:391-393。
  • 读请求:Read(handle, destList[], destCount, offset, sema|callback, ...),偏移必须 2 KB 对齐;标志 LOCK/UNLOCK/ENCRYPTED/UNCACHED/CRITICAL,CRITICAL 或设备被锁时进入关键队列 src/dev_ng/rage/base/src/paging/streamer.h:128-187、src/dev_ng/rage/base/src/paging/streamer.cpp:1204-1205。取消后完成回调/信号量仍会触发 src/dev_ng/rage/base/src/paging/streamer.h:118-126。
  • 处理:读线程把压缩数据读入读缓冲,处理线程用 zlibInflater 解压到目标块(加密资源先解密),也支持直接拷贝到显存(CopyVram) src/dev_ng/rage/base/src/paging/streamer.cpp:1372-1404、src/dev_ng/rage/base/src/paging/streamer.cpp:1491-1503、src/dev_ng/rage/base/src/paging/streamer.cpp:1600。
  • 初始化:InitClass(cpu, prio=PRIO_HIGHEST, cpu2) 只执行一次,初始化请求池、两个设备处理器、AES 对象;调试参数 -logstreaming(CSV 日志 + 刷新线程)、-slowstreaming、-emulateoptical、-singlequeue src/dev_ng/rage/base/src/paging/streamer.cpp:2161-2211、src/dev_ng/rage/base/src/paging/streamer.cpp:199-201。
  • 观测回调:Open/Read/Idle/ReadBulk/BeginProcess/Process/Decompress/ErrorDump 等回调供上层统计与调试 src/dev_ng/rage/base/src/paging/streamer.h:38-48、src/dev_ng/rage/base/src/paging/streamer.h:257-313。

streamer.cpp 中还存在与防篡改相关的成员(例如初始化时清零的 sm_tamperCrash),按安全边界本页不展开,见 23-debug-security/security-overview.md。


6. pgDictionary<T>:按哈希命名的资源集合#

纹理字典(.ytd)、可绘制体字典、剪辑字典等都是 pgDictionary<T>:

  • 数据:atArray<u32> m_Codes(有序哈希)与 atArray<datOwner<T>> m_Entries(并行数组),父字典指针 m_Parent、引用计数 src/dev_ng/rage/base/src/paging/dictionary.h:444-451。
  • 键:ComputeHash(name) = atStringHash(name)(大小写不敏感,见 math-and-containers.md) src/dev_ng/rage/base/src/paging/dictionary.h:298-301。
  • 查找:LookupLocal 在本字典二分查找;Lookup 沿 m_Parent 链逐级查找(例如“纹理字典父链”,模型纹理找不到时去父 TXD) src/dev_ng/rage/base/src/paging/dictionary.h:261-296。
  • 插入:AddEntry(code, data) 线性找位置后插入保持有序,重复哈希返回 false src/dev_ng/rage/base/src/paging/dictionary.h:333-371。
  • “当前字典”栈:sm_CurrentDict 为 TLS(DICTIONARY_THREAD = __THREAD,因 ENABLE_MTPLACEMENT_CODE 1),Push()/Pop() 构成链;资源构造某模型时,外层先把其纹理字典压栈,使模型内纹理引用在放置时能解析(源码称之为 “EVIL C++ VOODOO” 的辅助类) src/dev_ng/rage/base/src/paging/dictionary.h:19-25、src/dev_ng/rage/base/src/paging/dictionary.h:134-207、src/dev_ng/rage/base/src/paging/dictionary.h:454-460。
  • 析构时 Release 每个条目,并断言没有悬挂引用 src/dev_ng/rage/base/src/paging/dictionary.h:69-81。

约束与边界行为#

  • 资源版本在 RPF 中只保留 8 位:代码版本变化若 & 255 相同则无法检出;SaveBuild 拒绝 version & 255 == 0。
  • GetFixup 找不到所属块时直接 Quitf(ERR_SYS_INVALIDRESOURCE_5)——损坏或版本不符的资源表现为崩溃而非加载失败。
  • datResource::Fixup 在没有当前修复上下文时会把指针置 NULL:普通(非资源)构造路径中误用资源构造函数会清空指针。
  • 单个资源最多 128 块;页表 log2(size/leaf) 必须 < 16。
  • 修复栈与当前字典均为 TLS,允许多线程并行放置,但同一线程上的 datResource 必须严格后进先出。
  • 碎片整理/已知引用在本配置下关闭,因此资源内存在生命周期内不移动;任何依赖 pgHandle 双重间接的代码实际退化为直接指针。

未决问题 / 证据不足#

  • pgStreamer 处理线程的完整状态机、解密与 CopyVram 的触发条件、pgRawStreamer 细节只做了入口级确认。
  • singlePassAllocator 打包算法(桶选择、maxAlloc 等,见 src/dev_ng/rage/base/src/paging/rscbuilder.cpp:1028-1061)未逐行分析。
  • 未确认游戏发行配置下 ENABLE_BUDDY_ALLOCATOR 与 SupportsAllocateMap() 的实际取值(属于 01-runtime/memory-and-pools.md 范围)。