场景图与可见性:fwscene(世界表示 / 扫描 / 搜索 / 地图数据)
本页解释 RAGE Framework 的 fwscene 目录如何回答三个问题:
- 世界里有什么、放在哪? ——
fwWorldMgr→fwWorldRepMulti→ 场景图(fwSceneGraphNode树)→ 实体容器(fwBaseEntityContainer)→ 紧凑的实体描述符fwEntityDesc。 - 这一帧每个渲染阶段能看到什么? ——
fwScan把一个“节点扫描”主作业(fwScanNodes)和若干“实体扫描”子作业(fwScanEntities)投进依赖调度器,产出带每阶段可见性位的 PVS(fwPvsEntry)。 - 某个体积里有哪些实体? ——
fwSearch用同一套场景图/描述符做异步或同步的空间查询。
此外,fwscene/mapdata 与 fwscene/stores 提供 IMAP/ITYP 等地图资产的框架层存储(fwMapDataStore、fwBoxStreamer 等),它们把流式加载与世界表示连接起来。
背景与相关页面#
- 实体基类、
fwEntity::m_container、fwDynamicEntityComponent::m_entityDesc:见 entity-and-archetype.md。 - 资产存储(
fwAssetStore)与流式请求/状态机:见 streaming-engine.md;fwPool、fwDescPool、fwConfigManager:见 framework-systems.md。 - 游戏侧如何使用这些结构(
CGameWorld、IMAP/ITYP 内容、LOD 层次、内景、场景流送器):见 ../05-world/world-and-entities.md、../05-world/map-data-and-lod.md、../05-world/interiors-and-portals.md、../05-world/game-streaming-and-scene-streamer.md。 - 渲染阶段如何生成 cull shape、PVS 之后如何建渲染列表:见 ../11-rendering/render-pipeline-and-phases.md。HiZ 软件光栅遮挡缓冲的生产方:见 ../03-rage-graphics/postfx-shadows-and-occlusion.md。
- 依赖调度器
sysDependency与线程模型:见 ../01-runtime/threading-and-render-thread.md。
相关源码文件#
| 文件 | 内容 |
|---|---|
fwscene/world/WorldMgr.h/.cpp、WorldRepBase.h、WorldRepMulti.h/.cpp | 世界管理器、世界表示接口与唯一实现 |
fwscene/world/SceneGraph.h/.cpp、SceneGraphNode.h/.cpp、SceneGraphVisitor.* | 场景图池、节点基类、非递归 DFS 访问器 |
fwscene/world/ExteriorSceneGraphNode.*、StreamedSceneGraphNode.*、InteriorSceneGraphNode.* | 五种节点类型 |
fwscene/world/EntityContainer.h/.cpp、EntityDesc.h/.cpp | 容器(默认/固定/SoA/跨门户)与描述符 |
fwscene/world/InteriorLocation.h/.cpp、WorldLimits.h | 内景位置编码、世界边界 |
fwscene/scan/Scan.h/.cpp、ScanNodes.*、ScanEntities.*、*.frag | 可见性入口、主作业、子作业、SPU 片段 |
fwscene/scan/RenderPhaseCullShape.h、VisibilityFlags.h、ScanResults.h、ScreenQuad.*、ScanCascadeShadows.*、ScanDebug.* | cull shape、可见性位、结果、屏幕四边形、CSM 裁剪、调试开关 |
fwscene/search/Search.*、SearchNodes.*、SearchEntities.*、SearchVolumes.* | 世界搜索 |
fwscene/mapdata/*(含 mapdata.psc、maptypes.psc、mapinstancedata.psc) | IMAP/ITYP 数据定义与实例化 |
fwscene/stores/* | 各类 fwAssetStore 派生、fwBoxStreamer、fwImapGroupMgr、fwPsoStoreLoader |
fwscene/lod/* | fwLodData、LOD 附着、容器 LOD |
游戏侧装配:game/scene/world/GameWorld.cpp、game/scene/world/GameScan.cpp、game/renderer/PostScan.*、game/scene/scene.cpp | 注册世界表示、生成 cull shape、消费 PVS |
1. 模块地图#
图中证据:世界管理器把实体按类型/操作掩码转发给世界表示 src/dev_ng/rage/framework/src/fwscene/world/WorldMgr.cpp:115-161;fwWorldRepMulti 把一切委托给根外景节点 src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:29-51;外景节点把实体分流进容器 src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:134-150;容器从 fwDescPool 取描述符存储 src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:37-46、src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:19-30;游戏侧启动扫描 src/dev_ng/game/scene/world/GameScan.cpp:614-633;节点作业为每个容器投放实体作业 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:335-400;PostScan 在扫描进行中消费结果块 src/dev_ng/game/renderer/PostScan.cpp:528-559;搜索经世界表示进入 fwSearch src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:53-57;IMAP 内容创建流式节点并加入世界 src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatacontents.cpp:322-426。
2. 世界表示#
2.1 fwWorldMgr 与 fwWorldRepBase:可插拔世界表示#
fwWorldMgr 维护一个 fwWorldRepBase* 链表;每个世界表示声明自己处理的实体类型掩码和操作掩码。操作编号与掩码如下 src/dev_ng/rage/framework/src/fwscene/world/WorldRepBase.h:35-57:
| 操作 | 值 | 在 fwWorldMgr 中的分发 |
|---|---|---|
OP_ADD_REMOVE | 1 | AddToWorld/RemoveFromWorld(需同时 IsTypeHandled)以及 AddSceneNodeToWorld/RemoveSceneNodeFromWorld(不看类型) src/dev_ng/rage/framework/src/fwscene/world/WorldMgr.cpp:115-161、src/dev_ng/rage/framework/src/fwscene/world/WorldMgr.cpp:264-298 |
OP_PRESCAN / OP_POSTSCAN | 2 / 4 | PreScan/PostScan src/dev_ng/rage/framework/src/fwscene/world/WorldMgr.cpp:327-355 |
OP_SCAN | 3 | 仅作为掩码存在;实际扫描由 fwScan 直接拿根节点执行(见 §3) |
OP_SEARCH | 5 | ForAllEntitiesIntersecting src/dev_ng/rage/framework/src/fwscene/world/WorldRepBase.h:91 |
OP_UPDATE | 6 | UpdateInWorld src/dev_ng/rage/framework/src/fwscene/world/WorldMgr.cpp:213-260 |
OP_MISC / OP_DEBUG | 7 / 8 | GetAll、BANK 调试 |
判定函数是位测试:IsTypeHandled(type) 测 1<<type,IsOperationHandled(op) 测 1<<op src/dev_ng/rage/framework/src/fwscene/world/WorldRepBase.h:114-124。搜索模块另有一组调用方标识位 WORLDREP_SEARCHMODULE_*(音频、默认、行人、门户、脚本、流式、车辆 AI、调试、回放),用于统计/过滤 src/dev_ng/rage/framework/src/fwscene/world/WorldRepBase.h:59-73。
装配点(游戏侧):CGameWorld 初始化时先通过 fwEntityContainer::SetUpdateEntityDescFunc(CGameWorld::UpdateEntityDesc) 注入“实体→描述符”的填充函数,然后 new fwWorldRepMulti,设置它处理 Building/AnimatedBuilding/Vehicle/Ped/Object/DummyObject/Portal/MLO/NotInPools/ParticleSystem/Light/Composite 等类型以及全部操作,再 gWorldMgr.Initialise(pDefaultRep) src/dev_ng/game/scene/world/GameWorld.cpp:411-449。Initialise 会清空并只追加这一个表示 src/dev_ng/rage/framework/src/fwscene/world/WorldMgr.cpp:68-84;全仓库除 Initialise 外没有其他 AddWorldRep 调用者(rg "AddWorldRep\(" 仅命中 WorldMgr.h/.cpp)。这表明“多世界表示”框架在 GTAV 中只有一个实例。
2.2 fwWorldRepMulti#
构造时重置描述符池并 new 一个 fwExteriorSceneGraphNode 作为静态根 m_sceneGraphRoot src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:13-23。各接口的实现:
| 接口 | 实现 | 证据 |
|---|---|---|
AddToWorld / RemoveFromWorld | 直接调用根节点 AppendEntity / RemoveEntity(rect 参数被忽略) | src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:29-35 |
UpdateInWorld | 实体所属容器在外景节点下 → 根节点 UpdateEntity;否则(房间/门户/流式)→ 所属容器 UpdateEntityDescriptorInNode | src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:37-43 |
AddSceneNodeToWorld / Remove… | 根节点 AddChild/RemoveChild | src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:45-51 |
ForAllEntitiesIntersecting | 先 FlushUpdateList() 再 fwSearch::ForAllEntitiesIntersecting | src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:53-57 |
PreScan | DefragEntityDescPool(m_bNewDefrag, false)(BANK 下可“慢速”每 16 帧一次)后 FlushUpdateList() | src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:59-69 |
GetAll | 只返回根节点的静态容器与动态容器中的实体;源码注释自己承认名不副实 | src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:74-110 |
注意 PostScan 是空实现 src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:71-72。
2.3 场景图节点类型#
所有节点派生自 fwSceneGraphNode:成员只有 m_next(兄弟)、m_firstChild、u8 m_type、fwFlags8 m_flags、s16 m_index(32 位 12 字节 / 64 位 24 字节)src/dev_ng/rage/framework/src/fwscene/world/SceneGraphNode.h:31-35、src/dev_ng/rage/framework/src/fwscene/world/SceneGraphNode.h:97。类型枚举 src/dev_ng/rage/framework/src/fwscene/world/SceneGraphNode.h:10-18 与基础标志(含生成点、IS_IN_WORLD、ENABLED)src/dev_ng/rage/framework/src/fwscene/world/SceneGraphNode.h:24-29:
| 类型 | 类 | 关键成员 | 池容量(编译期) | 尺寸 32/64 |
|---|---|---|---|---|
EXTERIOR | fwExteriorSceneGraphNode | 静态 fwEntityContainer、动态 fwFixedEntityContainer、灯光 fwEntityContainer、fwStraddlingPortalsContainer、灯光更新列表 src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.h:17-22 | 1 | 48/80 src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.h:56 |
STREAMED | fwStreamedSceneGraphNode | m_firstContainer 链表、spdAABB m_aabb、SCENE_NODE_FLAG_CONTAINS_HIGH_DETAILS=1<<3 src/dev_ng/rage/framework/src/fwscene/world/StreamedSceneGraphNode.h:14-20 | 1000 | 48/64 src/dev_ng/rage/framework/src/fwscene/world/StreamedSceneGraphNode.h:47 |
INTERIOR | fwInteriorSceneGraphNode | s16 m_interiorIndex src/dev_ng/rage/framework/src/fwscene/world/InteriorSceneGraphNode.h:11-28 | 150 | 16/(Orbis 24, 其他 32) |
ROOM | fwRoomSceneGraphNode | 容器、所属内景、fwInteriorLocation、m_exteriorVisibilityDepth、方向光/禁用外景位 src/dev_ng/rage/framework/src/fwscene/world/InteriorSceneGraphNode.h:30-41 | 256 | 28/48 |
PORTAL | fwPortalSceneGraphNode | fwPortalCorners、容器、正/负两端节点指针 src/dev_ng/rage/framework/src/fwscene/world/InteriorSceneGraphNode.h:124-129 | 400 | 96/128 src/dev_ng/rage/framework/src/fwscene/world/InteriorSceneGraphNode.h:204-206 |
池容量与全局索引区间定义在 fwSceneGraph 中:容器池 ENTITY_CONTAINER_POOL_SIZE=800、FIXED_…=2、SOA_…=1000;节点按 Exterior→Streamed→Interior→Room→Portal 顺序连续编号,MAX_SCENE_NODE_COUNT = 1+1000+150+256+400 = 1807 src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.h:108-126。ComputeSceneNodeIndex 用“指针减池起始地址 + 区间偏移”计算这个全局索引 src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.cpp:202-222——扫描作业用它索引每节点的可见性位数组(见 §3.4)。
门户节点复用 fwFlags8 的高位存放 IS_ONE_WAY、RENDER_LODS_ONLY 以及 3 位镜像类型(NONE/MIRROR/PRIORITY_1..3/FLOOR/WATER_SURFACE),而“链接门户”“镜子能看到方向光/外景”“允许关闭”这几个标志塞在门户角点向量的 w 分量里,0 号角点的 w 存不透明度 src/dev_ng/rage/framework/src/fwscene/world/InteriorSceneGraphNode.h:83-122、src/dev_ng/rage/framework/src/fwscene/world/InteriorSceneGraphNode.h:166-196。
树形状:根是唯一的外景节点;流式节点(每个已加载 IMAP 一个)和内景节点都直接挂在根下(fwWorldRepMulti::AddSceneNodeToWorld → 根 AddChild;CInteriorInst 直接 GetSceneGraphRoot()->AddChild(m_interiorSceneNode));房间和门户节点挂在内景节点下 src/dev_ng/game/scene/portals/InteriorInst.cpp:3614-3632、src/dev_ng/game/scene/portals/InteriorInst.cpp:3669、src/dev_ng/game/scene/portals/InteriorInst.cpp:3746。AddChild 是尾插(遍历兄弟链表到末尾),并置 IS_IN_WORLD 位 src/dev_ng/rage/framework/src/fwscene/world/SceneGraphNode.cpp:50-69。fwSceneGraphVisitor 用显式栈做先序 DFS src/dev_ng/rage/framework/src/fwscene/world/SceneGraphVisitor.cpp:12-47。
简报中提到的 “world repository / quadtree” 在本代码中并不作为运行时世界表示存在:外景只有一个根节点,空间剔除靠流式节点自带的 AABB 和逐描述符测试完成。
Scan.h注释仍写着“主作业对外景做四叉树节点剔除”src/dev_ng/rage/framework/src/fwscene/scan/Scan.h:21-24,而fwScanNodes::ProcessExteriorVisibility实际是对外景子树做 DFS,并用流式节点 AABB 做IsClipped测试src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1066-1098。这是注释与实现的差异;WorldLimits.h中WORLDLIMITS_REP_*(±8192)注释提到的“top quad tree node”也只剩常量src/dev_ng/rage/framework/src/fwscene/world/WorldLimits.h:23-27。
2.4 场景图池与锁#
fwSceneGraph::InitPools() 把 8 个池(3 种容器 + 5 种节点)的对象存储与每池 flags 字节一次性分配在一块 rage_aligned_new(16) 连续内存上 src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.cpp:103-148。连续存储的目的是让扫描/搜索作业能把整个场景图作为单个输入缓冲 DMA 到 SPU:StartScanDependencies 把 GetPoolStorage()/GetPoolStorageSize() 作为参数 1 传入 src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:152-161,SPU 端再用 fwSceneGraph::InitSpuPools + fwPointerFixer 把 PPU 指针改写为本地地址 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:163-172、src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.h:30-50。
fwRoomSceneGraphNode 池被显式 LockPoolSize,防止 gameconfig.xml 覆盖其尺寸;注释说明这里绕过了宏生成的 InitPool(),所以其他池理论上也该锁但没有做 src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.cpp:154-162。另一方面,这些类的 FW_INSTANTIATE_CLASS_POOL[_SPILLOVER] 宏仍然存在(如容器的 spillover 0.62/0.27)src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:10-12、src/dev_ng/rage/framework/src/fwscene/world/StreamedSceneGraphNode.cpp:5,但 InitPools 直接 rage_new fwPool<...>(size, storage, flags, name) 覆盖了 _ms_pPool src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.cpp:141-148。这表明 spillover 比例对这些池不生效(推断:未追踪宏内的 InitPool 是否在别处被调用)。
游戏侧初始化顺序:CScene::Init 中 fwEntityContainer::InitEntityDescPool() → fwSceneGraph::InitPools() src/dev_ng/game/scene/scene.cpp:420-421。
锁:fwSceneGraph::LockSceneGraph()/UnlockSceneGraph() 是一个计数器(非互斥量)src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.h:128-131;所有修改操作开头都有 SceneGraphLockedFatalAssert(),在锁计数 > 0 时 fatal assert src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.h:15。扫描(StartScan→JoinScan)与搜索(Start→Finalize)期间各自加锁 src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:254、src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:291、src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:283、src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:319。因此作业运行期间主线程不得增删实体/节点;这一约束只在 assert 构建下被检查,不提供真正的线程同步。
2.5 外景根节点的容器分流#
fwExteriorSceneGraphNode 构造时创建四个容器:静态(增长步长 100)、动态(固定 2560 槽)、灯光(步长 64)、跨门户(64 槽)src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:10-15、src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:105-117。
| 操作 | IS_DYNAMIC 实体 | IS_LIGHT 实体 | 其他(静态) |
|---|---|---|---|
AppendEntity | 进固定容器;把返回的描述符指针写入 fwDynamicEntityComponent::SetEntityDesc(O(1) 定位) | 按实体指针有序插入灯光容器(二分 + memmove) | 追加到静态容器末尾 |
RemoveEntity | 用组件里缓存的描述符直接释放槽 | 二分定位后 memmove 并 PopEntity | 线性查找后与末尾交换 |
UpdateEntity | 立即调用 UpdateEntityDescFunc 重写描述符 | 延迟:推入 m_lightsUpdateList,在 FlushUpdateList 中排序去重后归并更新 | 线性查找后重写 |
证据:src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:134-171、src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:195-214、有序插入/删除/批量刷新工具函数 src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:19-101、静态容器删除 src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:433-463、静态容器更新 src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:521-535。灯光有序存放的作用是让 FlushUpdateListForContainer 能用一次归并扫描完成批量更新(排序后的更新列表与按指针有序的描述符数组同步前进)。FlushUpdateList() 在 PreScan 和每次搜索前调用,保证作业读到的灯光描述符是最新的 src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:55、src/dev_ng/rage/framework/src/fwscene/world/WorldRepMulti.cpp:68。
2.6 实体容器与 fwEntityDesc#
容器类型#
fwBaseEntityContainer 只保存一个 fwDescPoolAlloc* 句柄、缓存的描述符数组指针、单链 m_next、所属节点、14 位实体计数 + 2 位类型、缓存容量 src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:37-46。描述符本体都分配自一个全局 fwDescPool(默认 64000 个描述符,16 字节对齐,MEMBUCKET_WORLD)src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:23-28、src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:73-75。
| 类型 | 用途 | 存储策略 | 证据 |
|---|---|---|---|
fwEntityContainer(DEFAULT) | 外景静态/灯光、房间、门户、-noentitydescsoa 下的流式容器 | 按 m_storageStep 增长,删除时与末尾交换;空闲超过一个步长时收缩 | src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:407-463 |
fwFixedEntityContainer(FIXED) | 外景动态实体 | 固定容量;空闲槽链表复用描述符的 lodDistBackup 字段存“下一个空闲下标”;扫描时按 GetStorageSize() 全量遍历、跳过空指针 | src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:249-281、src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:566-588 |
fwStraddlingPortalsContainer | 跨越内外景门户的实体 | 固定 64 槽,u8 下标 | src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:283-290 |
fwSoAEntityContainer(SOA) | IMAP 流式实体(默认路径) | 每容器 ≤ SCAN_MAX_ENTITIES_PER_JOB(320),按 4 个一组以 SoA 布局存放,容量圆整到 4 | src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:292-339、src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:677-749 |
容器分配/扩容失败是致命的:Quitf(ERR_GEN_ENT_CONT_1/2/3, …pool is either full or fragmented) src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:19-70。由于描述符池会碎片化,每帧 PreScan 都做两轮 DefragPartialNew()(或旧算法 DefragPartial()),之后 UpdateDataHandleCacheForAll() 刷新各容器缓存的数组指针 src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:90-112。描述符可能被 defrag 移动,所以不能长期持有 fwEntityDesc*——唯一例外是动态实体组件里缓存的那一个,它位于固定容器内,固定容器创建后不再调整大小(推断:固定容器只在构造时 CreateDataHandle(size) src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:539-544;fwDescPool 的 defrag 是否会移动固定容器的块未进一步核实)。
fwEntityDesc 布局(32 位 48 字节 / 64 位 64 字节)#
描述符是可见性/搜索作业所需实体信息的快照,作业只读它,不碰 fwEntity 本体(除最后写入 PVS 的指针)src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:154-167:
| 字段 | 类型 | 含义 |
|---|---|---|
m_pEntity | fwEntity* | 注释:只用于 prerender 和最终绘制请求 |
m_phaseVisibilityMask | fwFlags16 | 实体允许出现的渲染阶段掩码(见下文常量) |
m_visibilityType | u16 | _GTA5_VIS_ENTITY_*:PED=1、OBJECT=2、VEHICLE=4、LOLOD_BUILDING=6、HILOD_BUILDING=7 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.h:90-97 |
m_nLodDistBackup | u16 | 缓存的 LOD 距离 |
sFlags | fwEntityDescFlags(16 位) | 实体类型 4 位、不剔除小阴影、NeverDummy、门、可见、可搜索、LOD 类型 3 位(注释:矢量代码依赖其位置)、车内行人、根 LOD、HD 纹理、跨门户 src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:57-71 |
m_positionAndUnused | Vec4V | AABB 中心 |
m_extentAndRadiusAndVecToLodAndFlags2 | Vec4V | 8 个 float16:半尺寸 xyz + 包围球半径,以及“到 LOD 父节点枢轴的向量”xyz + flags2(水反射最大距离、程序化实体) |
打包由 SetApproxBoundingBoxAndSphereAndVecToLod 完成,半径取“球半径 + 球心到盒心距离”以保证以盒心为中心的球包住原球 src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:169-183;GetLodPosition() = 中心 + 解包的 vecToLod src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:205-209。描述符由游戏层 CGameWorld::UpdateEntityDesc 填写:动态实体用扩展包围盒,灯光用盒心作枢轴(注释解释灯光变换为单位矩阵)src/dev_ng/game/scene/world/GameWorld.cpp:5779-5839。
渲染阶段常量(_GTA5_VIS_PHASE_*):GBUF=1、CASCADE_SHADOWS=2、PARABOLOID_SHADOWS=3、PARABOLOID_REFLECTION=4、WATER_REFLECTION=5、MIRROR_REFLECTION=6,代理标志位 SHADOW_PROXY=10、WATER_REFLECTION_PROXY=11、…_PRE_REFLECTED=12,以及对应 _MASK_ = 1<<值 src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:24-50。
源码缺陷:
_GTA5_VIS_PHASE_MASK_REFLECTIONS被定义为_GTA5_VIS_PHASE_PARABOLOID_REFLECTION | _GTA5_VIS_PHASE_MIRROR_REFLECTION,即4|6 = 6,用的是索引而非掩码(应为MASK_PARABOLOID_REFLECTION|MASK_MIRROR_REFLECTION)src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:52-54。它只在 BANK 的GetIsReflectionProxy()中使用src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:113-115,结果是该调试判断实际测试的是 bit1|bit2(GBUF/CSM 掩码位)。不影响非 BANK 构建。
SoA 形式 fwEntityDescSoA 把同一组字段拆成 12 个数组指针(x/y/z/extentXY/… 为 Vec4V 数组,4 个实体一组),SIZEOF_ENTITY_DESC_SOA 为 40(32 位)/44(64 位)字节每实体,且有编译期断言提醒同步 src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:292-312、src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:364。SoA 让 fwScanEntities 能一次对 4 个实体做矢量化剔除(入口 ProcessSingleEntityDescSoa src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2240,未逐行展开)。
2.7 流式节点:IMAP 内容如何进入世界#
fwMapDataContents::Entities_AddToWorld 为每个已加载 IMAP 创建一个 fwStreamedSceneGraphNode(HD 标志取自 CONTENTFLAG_ENTITIES_HD,bCulled 决定初始禁用),设置其 AABB 为 IMAP 的物理范围;默认按 320 个有效实体一块切分为多个 SoA 容器,-noentitydescsoa 时退回单个普通容器 src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatacontents.cpp:322-408。若加入被推迟(bPostponed,如 IMAP 组切换预加载),只清除实体的 IS_VISIBLE 而不挂入场景图;否则 gWorldMgr.AddSceneNodeToWorld src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatacontents.cpp:410-424。移除时逐容器 RemoveAllEntities → DestroyAllStreamedContainers → 若在世界中则摘除 → delete 节点 src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatacontents.cpp:432-452。
源码缺陷:
fwStreamedSceneGraphNode::RemoveStreamedContainer在删除链表首元素时写的是m_firstContainer = current(应为current->GetNext()),即首元素删不掉;若目标不在链表中会解引用空指针src/dev_ng/rage/framework/src/fwscene/world/StreamedSceneGraphNode.cpp:37-57。全仓库没有该函数的调用者(移除路径用的是DestroyAllStreamedContainerssrc/dev_ng/rage/framework/src/fwscene/world/StreamedSceneGraphNode.cpp:59-72),所以是潜伏缺陷。
2.8 fwInteriorLocation 与内景节点#
fwInteriorLocation 是 4 字节联合体:s16 m_interiorIndex + 1 位“挂在门户上”+ 1 位“门户通向外景”+ 14 位房间/门户下标;上限常量 INTLOC_MAX_INTERIOR_INDEX=1200、INTLOC_MAX_INTERIOR_ROOMS_COUNT=32、INTLOC_MAX_INTERIOR_PORTALS_COUNT=256、INTLOC_MAX_ENTITIES_IN_ROOM_0=12,房间 0 是“limbo” src/dev_ng/rage/framework/src/fwscene/world/InteriorLocation.h:6-28、src/dev_ng/rage/framework/src/fwscene/world/InteriorLocation.h:59-68。扫描以 fwScan::SetInitialInteriorLocation 指定起点(通常为主相机所在位置)src/dev_ng/rage/framework/src/fwscene/scan/Scan.h:83-85,cull shape 自带起始节点(外景根或某房间节点)。内景实例、门户追踪与房间语义由游戏层负责,见 ../05-world/interiors-and-portals.md。
fwscene/interiors/PortalTypes.h 只定义若干距离常量(PORTAL_HI_LOD_DIST=20、PORTAL_INSTANCE_DIST=40、MLO_LOD_DIST=80 等)src/dev_ng/rage/framework/src/fwscene/interiors/PortalTypes.h:15-27。
3. 可见性扫描(fwScan)#
3.1 设计#
Scan.h 的设计说明:扫描完全作业化;一个主作业(ScanNodes)遍历容器层次并为实体派生子作业;外景实体依次经过“早期拒绝(距离)→ 阶段排除(类型/LOD 等)→ 视锥裁剪 →(旧的几何遮挡)→ 光栅遮挡”,内景实体只做“阶段排除 → 门户产生的视锥裁剪”;各阶段对每个渲染阶段独立执行 src/dev_ng/rage/framework/src/fwscene/scan/Scan.h:16-38。
关键容量 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.h:41-48、src/dev_ng/rage/framework/src/fwscene/scan/ScanDebug.h:10-12、src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.h:26-32、src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.h:305-307:
| 常量 | 值 | 含义 |
|---|---|---|
SCAN_RENDER_PHASE_COUNT | 24 | 最多 cull shape 数;也等于 fwVisibilityFlags 的阶段位宽 |
SCAN_MAX_ENTITIES_PER_JOB | 320 | 每个实体作业的描述符数(注释:48 字节 × 320 填满 16KB SPU 输入缓冲) |
SCAN_NUM_PVS_ENTRIES_PER_RESULT_BLOCK | 32 | 子作业每积累 32 条 PVS 就提交一个结果块 |
MAX_SCAN_ENTITIES_DEPENDENCY_COUNT | 360/PS3: 500;其他: 2048 | 每帧实体作业上限(预分配的 sysDependency 数组) |
fwScanNodes::SCRATCH_BUFFER_SIZE | 50 KB | 主作业暂存(含 SPU 自定义栈) |
PORTAL_STACK_SIZE / GBUFFER_ROOM0_NODES | 192 / 8 | 门户遍历栈 / GBuffer 可见 room0 节点数 |
fwScanEntities::SCRATCH_BUFFER_SIZE | 16 KB | 子作业暂存 |
游戏 MAX_PVS_SIZE / MAX_RESULT_BLOCKS | PC/Durango/Orbis: 32000 / 2×2048;旧主机: 7000 / 500 | PVS 输出数组与结果块数组 src/dev_ng/game/renderer/PostScan.h:39-45 |
全局状态都在 Scan.cpp 顶部:g_scanBaseInfo(128 对齐)、g_screenQuadStorage[4][STORED_SCREEN_QUAD_COUNT]、待定/当前两份 fwScanResults、一个节点依赖 + MAX_SCAN_ENTITIES_DEPENDENCY_COUNT 个实体依赖、g_scanTasksPending 计数器,以及仅 PC 的 g_scanTasksReady 事件 src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:44-56。
3.2 cull shape:渲染阶段的可见性描述#
每个参与扫描的渲染阶段向 fwScan::AddCullShape() 提交一个 fwRenderPhaseCullShape(≤24 个,超出 assert)src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:204-208。字段:8 平面转置视锥、球、内景球、合成矩阵、相机位置、镜面平面、阶段 id、起始场景节点、实体可见性掩码、可见性类型、标志、遮挡/屏幕四边形存储 id、每 LOD 级的距离区间 m_lodRanges[7][2]、最大行人/车辆/物体距离平方、LOD 掩码 src/dev_ng/rage/framework/src/fwscene/scan/RenderPhaseCullShape.h:60-77(32/64 位 352/368 字节 src/dev_ng/rage/framework/src/fwscene/scan/RenderPhaseCullShape.h:161)。IsClipped(box) = 既不与视锥相交也不与球相交(按几何标志选择)src/dev_ng/rage/framework/src/fwscene/scan/RenderPhaseCullShape.h:147-158。
fwCullShapeFlags(29 位)src/dev_ng/rage/framework/src/fwscene/scan/RenderPhaseCullShape.h:15-46,按用途归类:
| 类别 | 标志 |
|---|---|
| 几何 | GEOMETRY_FRUSTUM、GEOMETRY_SPHERE、USE_SEPARATE_INTERIOR_SPHERE_GEOMETRY |
| 角色 | PRIMARY(GBuffer)、CASCADE_SHADOWS、REFLECTIONS(抛物面反射)、MIRROR、WATER_REFLECTION(+_USE_SEPARATE_SPHERE、_PROXIES_ONLY)、HEIGHT_MAP |
| 渲染范围 | RENDER_EXTERIOR、RENDER_INTERIOR、RENDER_ONLY_IF_PRIMARY_RENDERS、RENDER_LOD_BUILDING_BASED_ON_INTERIOR、RENDER_ONLY_ROOT_LOD |
| 门户 | TRAVERSE_PORTALS、TRAVERSE_DISABLED_PORTALS、CLIP_AND_STORE_PORTALS、USE_SCREEN_QUAD_PAIRS、IGNORE_VERTICAL_MIRRORS、IGNORE_HORIZONTAL_MIRRORS |
| 距离/LOD | USE_LOD_RANGES_IN_EXTERIOR、USE_LOD_RANGES_IN_INTERIOR、USE_MAX_PED_VEH_OBJ_DISTANCES |
| 遮挡 | ENABLE_OCCLUSION、ENABLE_WATER_OCCLUSION |
| 流式 | STREAM_ENTITIES、ONLY_STREAM_ENTITIES |
游戏侧 CGameScan::SetupScan 先设根节点、主 LOD 缩放、起始内景位置和两个 HiZ 缓冲,再遍历活动的扫描型渲染阶段,取 GetCullFlags() 构造 cull shape,并按绘制列表类型为 CLIP_AND_STORE_PORTALS 的阶段分配屏幕四边形存储(GBUF→PRIMARY、CSM→CASCADE_SHADOWS、镜子→MIRROR)src/dev_ng/game/scene/world/GameScan.cpp:173-226;timecycle 的“陈旧外景渲染”值近 0 时会清掉 RENDER_EXTERIOR src/dev_ng/game/scene/world/GameScan.cpp:202-210。具体有哪些阶段、各自的标志组合属于渲染页 ../11-rendering/render-pipeline-and-phases.md。流式用的 cull shape(HD/LOD/内景/流式体积/速度矢量流送器,带 ONLY_STREAM_ENTITIES)也在 GameScan.cpp 中生成(SetupSceneStreamingCullShapes_* 等函数位于 643–1127 行一带 src/dev_ng/game/scene/world/GameScan.cpp:643),属于 ../05-world/game-streaming-and-scene-streamer.md。
fwScan::SortCullShapes() 把带 CLIP_AND_STORE_PORTALS 的排前、带 TRAVERSE_PORTALS 的排前、PRIMARY 排在 CASCADE_SHADOWS 前;PC 因编译器在栈上生成未对齐的 cull shape 拷贝而改用 qsort,其他平台 std::sort(注释自己怀疑 PC 比较器不满足 qsort 约定)src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:169-198。
3.3 帧内时序#
图中证据:
CScene::Update调CGameScan::Updatesrc/dev_ng/game/scene/scene.cpp:848-849,后者fwScan::SetupDependencies()+gPostScan.Reset()src/dev_ng/game/scene/world/GameScan.cpp:582-591;SetupDependencies在 PPU 上默认用FRAG_SPU_CODE(ScanNodes),否则用fwScanNodes::RunFromDependency,并清零 pendingsrc/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:301-317。KickVisibilityJobs作为名为 "Visibility jobs early kick" 的更新调用注册src/dev_ng/game/Core/game.cpp:3006,内部顺序gWorldMgr.PreScan()→ 等光栅化器 →StartAsyncSearchForAdditionalPreRenderEntities→SetupScan→fwScan::StartScan()src/dev_ng/game/scene/world/GameScan.cpp:614-633;若 GBuffer 阶段不活动则只等光栅化器后返回src/dev_ng/game/scene/world/GameScan.cpp:621-626。StartScan加锁、(未开阴影遮挡时)让 CSM 共用主 HiZ、排序、投放主作业src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:243-263;主作业Critical优先级、pending 置 1src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:150-166。- 主作业投放子作业并在结束时自减
src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:350-400、src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:199;子作业结束自减并在 PC 上sysIpcSetEventsrc/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:153-156。 - 渲染列表构建:
CViewportManager::BuildRenderLists→gWorldMgr.PostScan()→CRenderer::IssueAllScanTasksNew()→gPostScan.BuildRenderListsNew()src/dev_ng/game/camera/viewports/ViewportManager.cpp:1048-1054;IssueAllScanTasksNew末尾调CGameScan::PerformScan()src/dev_ng/game/renderer/Renderer.cpp:1633;PerformScan先ProcessPvsWhileScanning再JoinScan,并在IsEverythingInShadow时禁用级联阴影与方向光src/dev_ng/game/scene/world/GameScan.cpp:593-612。 JoinScan自旋+sysIpcYield等 pending 归零,再g_currentScanResults = g_pendingScanResults并解锁src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:265-291;GetScanResults()断言调用者是更新线程src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:214-217。- PostScan 消费循环读
m_ResultBlockCount,不足时在 PC 上WaitForSingleObject(g_scanTasksReady, 1),其他平台sysIpcSleep(0)src/dev_ng/game/renderer/PostScan.cpp:528-559。
CGame::Update 中还保留了“非提前启动”路径:若 SCAN_DEBUG_EARLY_KICK_VISIBILITY_JOBS 未置位,则在 BuildRenderLists 前才 PreScan src/dev_ng/game/Core/game.cpp:3446-3460;默认调试标志包含该位 src/dev_ng/rage/framework/src/fwscene/scan/ScanDebug.h:171-176,即默认走提前启动。PostScan 的渲染列表、prerender 等后续处理见 ../11-rendering/render-pipeline-and-phases.md。
3.4 fwScanNodes:场景图遍历与门户#
主作业在暂存中布置:每节点一个 u32 可见性位(m_visFlags[MAX_SCENE_NODE_COUNT])、两份屏幕四边形存储(双缓冲)、正常/镜像两套“正在访问/已访问”位集、renderLodsOnly 位集、16 字节 0xFE 守卫 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:204-234。SPU 上还把暂存剩余部分当作自定义栈运行 Run() src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:184-189。
fwScanNodes::Run src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1224-1412:
- 清可见性数据,找到
PRIMARY与CASCADE_SHADOWScull shape(主阶段必须存在)。 - 对每个 cull shape:起始节点为外景 →
ProcessExteriorVisibility;为房间 → 记录m_startingSceneNodeInInterior、主阶段读取房间的“禁用外景”、设置方向光、按房间exteriorVisibilityDepth(-1 视为无限)调用ProcessRoomVisibilitysrc/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1287-1317。镜子且需穿门户时使用镜像遍历类型src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1281-1285。 - 若该 cull shape 带
CLIP_AND_STORE_PORTALS,把本阶段每节点的屏幕四边形写回g_screenQuadStorage[storageId](SPU 用 DMA),并切换双缓冲;主阶段还写出外景 GBuffer 屏幕四边形src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1325-1349。 - CSM 激活且不是“全在阴影里”时,把当前房间和 GBuffer 可见的 room0 节点加入 CSM 可见性
src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1353-1368;按需计算外景受限阴影视锥src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1376-1385。 ProcessEntityContainers()派生实体作业src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1409。
外景遍历 ProcessExteriorVisibility:DFS 外景子树;外景/流式节点需要 RENDER_EXTERIOR,内景类需要 RENDER_INTERIOR;流式节点用 AABB 做 IsClipped(反射阶段用 AABB 外接球与 cull 球相交测试);遇到内景节点先处理“从外景看进去”的门户可见性,并在 TRAVERSE_PORTALS 时把门户压栈后 TraversePortals,然后 SkipChildren src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1050-1121。AddVisibility 把阶段位或进 m_visFlags[节点]、按需扩展屏幕四边形,主阶段可见时置“外景在 GBuffer 可见”,CSM 下累积“可见房间阴影四边形” src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:425-467。
门户遍历 TraversePortals:深度 > 24 直接返回 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:550-553(与 fwSceneGraphVisitor::MAX_STACK_SIZE=24 一致 src/dev_ng/rage/framework/src/fwscene/world/SceneGraphVisitor.h:21-23)。对栈顶每个门户:镜像遍历时把角点关于镜面反射并反序 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:603-615;跳过禁用门户(除非 TRAVERSE_DISABLED_PORTALS);相机离门户“接近度” ≤ 0.05 时不做裁剪直接穿过,≤ 0.6 且为视锥几何时也不做视锥裁剪;否则对角点 AABB 做裁剪——反射阶段在房间里用内景球/外景球、CSM 可选扫掠视锥、主阶段对通向外景的门户把 AABB 放大 1m(注释:B*1876528 的近裁剪面 hack)src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:566-568、src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:630-693;非球几何时检查相机在门户正确一侧 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:695-702;CLIP_AND_STORE_PORTALS 时把当前屏幕四边形与门户投影求交,空则停止(镜像遍历除外)src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:704-710。镜子门户在主阶段还会更新镜子可见性结果(fwScanResults 的镜子相关标志 src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:133-146),细节未逐行展开。
派生实体作业 ProcessEntityContainers src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1123-1222:
- 先合成
primaryPhaseBit与RENDER_ONLY_IF_PRIMARY_RENDERS阶段位;节点若在主阶段不可见,就清掉这些“只在主阶段可见时渲染”的阶段位src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1129-1155。 - 外景节点:静态/动态/灯光三个容器,控制标志带
SCAN_ENTITIES_DONT_RUN_ON_PPU(该标志在全仓库没有读取者,推断是遗留);流式节点:遍历其容器链;房间/门户:带SCAN_ENTITIES_FLAG_INTERIOR,通向外景的门户再加PORTAL_TO_EXTERIORsrc/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1172-1212。 - 最后,若外景跨门户容器非空,就把它的节点可见位设为所有 cull shape 的阶段位并以
STRADDLING_PORTALS_CONTAINER|INTERIOR处理src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:1217-1221。
ProcessEntityContainer 把节点可见位(低 24 位)与控制标志(高 8 位)打包为一个 int 参数,按 ≤320 个描述符切片,每片:pending 自增 → 从预分配数组取下一个 sysDependency(超过上限只 assert)→ 设参数(g_scanBaseInfo、g_pendingScanResults、两块 HiZ、描述符切片、反射平面组、打包标志、屏幕四边形下标)与输入大小 → 插入调度器(SPU 上先 DMA 回写依赖结构)src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:327-400。固定容器按存储容量而非计数切片 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:333;流式节点禁用时整体跳过 src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:312-313。
源码缺陷:作业优先级判断写成
const bool bIsVisibleInGBuf = visFlags && (1 << m_gbufCullShape->GetRenderPhaseId());——逻辑与&&而非按位与&src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:331。由于此前已对!visFlags提前返回src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:321-322,该表达式恒为真,于是所有实体作业都以kPriorityCritical投放src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:377,与注释“只有 GBuffer 可见容器用 Critical,其余用 High”src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:368-376的意图不符。
3.5 fwScanEntities:逐实体判定#
RunFromDependency 解包参数:packedFlags 低 24 位为节点可见位(m_nodeVisTags),高 8 位为控制标志;从 g_screenQuadStorage 取 4 种存储中本节点的屏幕四边形 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:100-125。Run 先筛出本作业要测的 cull shape:跳过“全在阴影里”时的 CSM、外景在 GBuffer 不可见时的 HEIGHT_MAP 阶段、以及节点可见位不含的阶段;再为 CLIP_AND_STORE_PORTALS 阶段用屏幕四边形生成受限视锥(跨门户容器用全屏);最后按容器类型调用 ProcessEntityDescsSoA() 或 ProcessEntityDescs() 并提交剩余 PVS src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2494-2540、src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2596-2636。
ProcessEntityDescs(AoS 路径)对每个非空描述符 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2138-2238:
证据:早期拒绝与逐阶段循环 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2159-2213;选项位 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2215-2231;最终过滤 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2233-2236。注意“可流式”只要求可见(含被遮挡),不要求未遮挡 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2188-2210。
早期拒绝公式 IsEntityEarlyRejected src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:868-902:
fadeDist= 孤立 HD 且缓存 LOD 距离 ≤LODTYPES_FADE_DIST(20) 时为LODTYPES_FADE_SMALL_DISTF(5),否则LODTYPES_FADE_DISTF(20);常量见src/dev_ng/rage/framework/src/fwscene/lod/LodTypes.h:21-24。lodScale= 主阶段用m_perLodLevelScales[lodType],否则m_defaultLodScale;草批次实体再乘m_grassBatchLodScale。- 可见距离 =
lodDistCached × lodScale + fadeDist + preStream(preStream仅主阶段,取m_rendererPreStreamDistance),以 LOD 枢轴位置到相机距离平方比较。 - 另外拒绝:跨门户实体但当前不是跨门户容器;以及 timecycle 修改盒指定的
[m_fCullDistMin, m_fCullDistMax]之外。 - BANK 下有一组“只拒绝/不拒绝选中或列表实体”的调试覆盖
src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:904-967。
IsEntityVisible 的阶段(任一失败返回 0)src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:1986-2064:
| 步骤 | 函数 | 要点 |
|---|---|---|
| 1 阶段排除 | IsEntityExcludedInRenderPhase | 描述符不可见即排除;水反射阶段处理代理/非代理与“只代理”;LOD 掩码(灯光看 LODMASK_LIGHT,其余用 LodTypeToLodMask 把 HD/孤立 HD 映射到内/外景 HD 位、SLOD4 回退一位)src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:1007-1057、src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:979-1005;掩码位定义 src/dev_ng/rage/framework/src/fwrenderer/RenderPhaseBase.h:137-146 |
| 2 裁剪 | IsEntityClipped | 主阶段→主屏幕四边形受限视锥;CSM→阴影专用;反射→反射专用;镜子→镜子屏幕四边形;其余默认 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:1969-1983 |
| 3 LOD 区间 | IsEntityExcludedByLodRange | 仅当 cull shape 对内/外景开启 LOD 区间;RENDER_ONLY_ROOT_LOD 排除非根 LOD src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:1173-1200 |
| 4 行人/车/物体最大距离 | — | 仅 USE_MAX_PED_VEH_OBJ_DISTANCES 且描述符设置了“水反射用最大距离” |
| 5 遮挡 | IsEntityOccluded | 仅非内景容器、调试允许且 cull shape 开启遮挡;主阶段测主 HiZ,CSM 测阴影 HiZ(可再做投影阴影遮挡),水反射测 128×72 水面模板 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:1680-1741 |
遮挡结果不让函数返回 0,而是返回 VISIBLE|OCCLUDED——调用方据此“不设可见位但仍可流式” src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2059-2063。内景实体从不做遮挡测试 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:2056,与设计注释一致。
3.6 PVS 输出与结果块#
AddPvsEntry 写入 LOD 排序值、实体类型、按 CalcAlphaFade 计算的 alpha(淡入区间映射到 0..1 再乘 255,加 0.0001 防零)src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:171-182、src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:185-197;本地缓冲满 32 条就提交 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:210-214(注释说明这是为了让主线程更早开始处理,并提醒结果块数可能超过作业数)src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:199-209。
SubmitScanEntries 是无锁追加 + 临界区登记 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:217-280:
sysInterlockedAdd(m_pvsCurrentPtr, n*sizeof(fwPvsEntry))抢占输出区间,越界 FatalAssert(“increase MAX_PVS_SIZE”)。memcpy(SPU 上 DMA)写入 PVS。- 在静态临界区内取下一个结果块下标、写
{ptr, count},sysMemWriteBarrier()后++m_ResultBlockCount。注释说明以前用忙等保证顺序,在 PC 上造成卡顿,改为临界区。
数据结构:fwPvsEntry = 实体指针 + 距离 + fwVisibilityFlags(16/24 字节)src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:19-44、src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:241;fwScanResultBlock = 起始指针 + 条数 src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:46-52;fwScanBaseInfo 持有输出指针、结果块数组/计数器、LOD 缩放/淡入/排序表、m_lodFilterMask、m_streamingVolumeMask、最多 16 个关闭内景等 src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.h:141-214。
fwVisibilityFlags(8 字节)src/dev_ng/rage/framework/src/fwscene/scan/VisibilityFlags.h:27-88:
| 位段 | 宽度 | 含义 |
|---|---|---|
m_alpha | 8 | 淡入 alpha |
m_nPhaseVisFlags | 24 | 每渲染阶段可见位(下标 = cull shape 的 render phase id) |
m_nOptionFlags | 14 | VIS_FLAG_*:OPTION_INTERIOR、STREAM_ENTITY、ONLY_STREAM_ENTITY、RENDERED_THROUGH_LODS_ONLY_PORTAL、PED_INSIDE_VEHICLE、HD_TEX_CAPABLE、NEVER_DUMMY src/dev_ng/rage/framework/src/fwscene/scan/VisibilityFlags.h:17-25 |
m_nSubphaseVisFlags | 10 | 子阶段(如 CSM 各级联、反射面)可见位 |
m_nLodSortVal | 3 | LOD 排序值 |
m_entityType | 4 | 实体类型 |
fwScanResults 汇总整帧结果:外景阴影视锥平面(16 个,CASCADE_SHADOWS_SCAN_NUM_PLANES)、外景 GBuffer 屏幕四边形、可见房间阴影四边形、镜子角点/位置/距离/优先级、标志(外景在 GBuffer 可见、房间方向光、镜子/镜面地板可见、水面可见等)、天空门户(抛物面反射最多 64 个,注释:V_farmhouse 有 52 个)src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:129-167、src/dev_ng/rage/framework/src/fwscene/scan/ScanCascadeShadows.h:11-13。IsEverythingInShadow() = 外景在 GBuffer 不可见 且 无可见房间阴影四边形 且 房间未开方向光 src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:187。
3.7 平台与构建差异#
| 维度 | 差异 | 证据 |
|---|---|---|
| PS3 | 主/子作业以 SPU 片段(ScanNodes.frag/ScanEntities.frag)运行;场景图整块 DMA 后做指针修正;结果用 DMA 回写;可用 SCAN_DEBUG_PROCESS_VISIBILITY_ON_PPU 强制 PPU | src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:34-36、src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.frag:1-22、src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:305-314 |
| 360/PS3 | 实体依赖上限 500、PVS 7000(PC/新主机 2048 / 32000) | src/dev_ng/rage/framework/src/fwscene/scan/ScanDebug.h:11、src/dev_ng/game/renderer/PostScan.h:39-45 |
| PC | cull shape 用 qsort;子作业完成时 SetEvent,消费者 WaitForSingleObject(...,1) 替代忙等 | src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:183-197、src/dev_ng/game/renderer/PostScan.cpp:551-555 |
| PS3 | 屏幕四边形对可用定点表示(USE_FIXED_POINT_SCREEN_QUAD_PAIR) | src/dev_ng/rage/framework/src/fwscene/scan/ScreenQuad.h:9-10 |
| BANK | 大量 g_scanDebugFlags* 旁路开关、实体追踪、计时;SCAN_SKY_PORTALS_FOR_REFLECTION_EXT 仅 __DEV | src/dev_ng/rage/framework/src/fwscene/scan/ScanResults.h:54-60、src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.h:187-211 |
4. 世界搜索(fwSearch)#
设计注释:搜索作业化,可异步运行;输出为实体指针数组,ForAllEntitiesIntersecting 回调形式只为向后兼容;异步搜索期间不得修改世界 src/dev_ng/rage/framework/src/fwscene/search/Search.h:14-26。
| 组件 | 职责 | 证据 |
|---|---|---|
fwSearchConstants | 搜索体积 + 类型/位置/LOD/选项过滤 + 结果计数指针/容量/数组(16 字节对齐以便 DMA) | src/dev_ng/rage/framework/src/fwscene/search/Search.h:30-44 |
fwSearch | 一个搜索对象:常量、结果数组、一个 sysDependency、pending 计数 | src/dev_ng/rage/framework/src/fwscene/search/Search.h:54-82 |
fwSearchVolume | 8 种体积:球内/球相交/球相交且可见/球相交近似盒、盒内/盒相交近似/盒相交 BB、线段相交 BB | src/dev_ng/rage/framework/src/fwscene/search/SearchVolumes.h:28-38 |
| 过滤枚举 | 位置(外景/内景)、LOD(高/低细节)、选项(只要生成点、只要动态、强制 PPU、跳过程序化实体) | src/dev_ng/rage/framework/src/fwscene/search/SearchEntities.h:20-53 |
流程:
Start重置结果、准备常量、加锁场景图、投放一个SearchNodes_Dependency(默认kPriorityMed)src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:238-287、src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:195-219、src/dev_ng/rage/framework/src/fwscene/search/Search.h:114;__VALIDATE_SEARCH_VOLUMES下校验体积分量有限。fwSearchNodes::Process用fwSceneGraphVisitorDFS:外景节点处理静态+灯光(除非“只要动态”)与动态容器;流式节点按“只要动态/位置/HD-LOD/生成点/禁用”提前剪枝,再以搜索体积包围盒与节点 AABB 相交测试;房间/门户处理各自容器src/dev_ng/rage/framework/src/fwscene/search/SearchNodes.cpp:98-162。每个容器在同一作业内同步执行fwSearchEntities::Process,使用函数内static暂存(18KB)src/dev_ng/rage/framework/src/fwscene/search/SearchNodes.cpp:70-96、src/dev_ng/rage/framework/src/fwscene/search/SearchEntities.h:82。- 结果写入:非 SPU 每条
sysInterlockedIncrement抢下标,超出容量 assert 并把计数钳到容量(HEIGHTMAP_GENERATOR_TOOL下Quitf)src/dev_ng/rage/framework/src/fwscene/search/SearchEntities.cpp:144-160;SPU 先本地缓冲再 CAS + DMAsrc/dev_ng/rage/framework/src/fwscene/search/SearchEntities.cpp:110-139。 Finalize以 yield 自旋等待 pending 归零(assert 构建 20 秒超时告警),然后解锁src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:295-319。IsFinished()未实现(assert 后返回 true)src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:341-345。ForAllEntitiesIntersecting是同步包装:用全局默认搜索对象(容量 5120)以kPriorityCriticalStart+Finalize,再逐个回调,回调返回 false 即停止src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:43-47、src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:146-168、src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:347-367。注释说明改为 Critical 是因为调用线程(应总是主线程)在等待。
约束:默认搜索对象是全局单例,因此 ForAllEntitiesIntersecting 不可重入/不可并发(BANK 下 m_searchStarted 断言“Search already started”)src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:240-243。SearchEntities.frag 定义了 SearchEntities SPU 片段,但当前代码中节点作业直接在本作业内处理实体,未找到投放 SearchEntities 依赖的调用点(rg SearchEntities_Dependency 只命中定义与 frag)。
5. 地图数据(mapdata)与资产存储(stores)#
本节只描述框架层机制;IMAP/ITYP 的内容语义、LOD 层次与游戏派生类见 ../05-world/map-data-and-lod.md。
5.1 数据定义(parser 反射)#
mapdata.psc 定义 fwEntityDef(原型名哈希、标志、GUID、位置、四元数旋转、XY/Z 缩放、父索引、LOD 距离、子 LOD 距离、LOD 级别、子数量、优先级、扩展数组)与 fwMapData(名称、父 IMAP、标志、内容标志、流式范围、实体范围、实体数组、容器 LOD、盒/模型遮挡体、物理字典、实例化数据)src/dev_ng/rage/framework/src/fwscene/mapdata/mapdata.psc:24-86。
fwEntityDef::eLoadFlags:框架位 0–6(全矩阵、低优先级流式、不实例化碰撞、LOD 在父 IMAP 中、LOD 领养、固定、内景 LOD),游戏位 15–29(光源阴影、只/不在阴影/反射/水反射/镜子中渲染等)src/dev_ng/rage/framework/src/fwscene/mapdata/mapdata.h:39-66;优先级PRI_REQUIRED=0 … PRI_OPTIONAL_LOW=3src/dev_ng/rage/framework/src/fwscene/mapdata/mapdata.h:67-74。fwMapData::eContentTypes:HD/LOD/容器 LOD 实体、MLO、BLOCKINFO、遮挡体、物理实体、LOD 灯、远景 LOD 灯、关键实体、实例列表,以及若干组合掩码;eFlags:FLAG_MANUAL_STREAM_ONLY(仅脚本手动加载)、FLAG_IS_PARENTsrc/dev_ng/rage/framework/src/fwscene/mapdata/mapdata.h:113-136。fwInstancedMapData含道具实例列表与草实例列表(GPU 实例化绘制)src/dev_ng/rage/framework/src/fwscene/mapdata/mapinstancedata.h:198-208。
实体构造 fwMapData::ConstructEntity:流式原型必须来自该 IMAP 依赖的 .ityp(或永久 DLC 的 ityp),否则警告+断言并跳过;然后 archetype->CreateEntity()、SetModelId、InitEntityFromDefinition src/dev_ng/rage/framework/src/fwscene/mapdata/mapdata.cpp:69-149。Entities_Create 中优先级高于全局上限 ms_entityLevelCap(默认 PRI_OPTIONAL_LOW,即全部实例化)src/dev_ng/rage/framework/src/fwscene/mapdata/mapdata.cpp:29 且非固定的实体不实例化(MLO 实例不允许被剥离)src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatacontents.cpp:127-147;之后做 LOD 附着,其中有一段针对特定内景 LOD 的硬编码距离修补(注释引用 bug 号)src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatacontents.cpp:212-228。ITYP 原型构造走 fwMapTypes::ConstructArchetypes:按 parser 结构名哈希查工厂、CreateBaseItem(mapTypeDefIndex)、InitArchetypeFromDefinition src/dev_ng/rage/framework/src/fwscene/mapdata/maptypes.cpp:93-136(原型存储见 entity-and-archetype.md)。
5.2 fwMapDataStore#
fwMapDataStore : fwAssetStore<fwMapDataContents, fwMapDataDef>,模块名 "MapDataStore"、MAPDATA_FILE_ID、槽数完全来自配置(size 参数为 CONFIGURED_FROM_FILE=1,由 FinalizeSize 按模块名从 gameconfig 读取),第 4 个参数 455 是 elementsPerPage(仅分页流式池模式使用,见 streaming-engine.md)src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:81。它额外持有加载列表、fwBoxStreamerVariable、fwImapGroupMgr(IMAP 组切换)、fwPsoStoreLoader src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.h:127-137。游戏骨架通过 CInstanceStoreWrapper 的 Init/Shutdown1/Shutdown2 调它 src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.h:163-169。
| 阶段 | 行为 | 证据 |
|---|---|---|
Init(INIT_CORE) | box streamer 优先半径 30;注册 4 种 MAPDATA 资产,搜索半径 LODTYPES_FADE_DIST + IMAP_PRESTREAM_DIST(20+30);MEDIUM/LOW 为“总是优先”;PSO 加载器原位加载、布局不匹配告警+断言、根类型 fwMapData | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:106-126、src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:43-44 |
FinishLoading | 首次加载(未经缓存初始化)只 PreLoad 提取范围等元数据;已完全初始化则 contents->Load 创建实体、父 IMAP 子计数+1、进加载列表;最后 AddRef(REF_OTHER) 防止流式清理删除 | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:219-290 |
Update | IMAP 组切换更新 → RemoveUnrequired(true) → 切换清理 | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:509-516 |
RemoveUnrequired | 按依赖深度排序加载列表,移除满足“已加载、无已加载子、box streamer 允许、未被组切换使用、无 DONTDELETE 或被剔除、引用数==1、无依赖者”的 IMAP(非 bAll 时每帧最多 5 个) | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:534-596 |
Remove | 摘加载列表、内容 Remove、PSO 卸载、父子计数-1;其依赖的 .ityp 通过 gDrawListMgr->AddTypeFileReference 延迟到渲染线程不再引用后才可卸载 | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:396-433 |
ModifyHierarchyStatus | 上溯到根 IMAP 再递归修改整棵父子层次的流式状态 | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:691-716 |
fwMapDataDef::eInitState:NOT_INITIALISED → REQUIRES_PRELOAD(缓存加载器初始化过但仍需流入)→ FULLY_INITIALISED src/dev_ng/rage/framework/src/fwscene/mapdata/mapdatadef.h:33-38。缓存文件读写(ReadFromCacheFile/AddToCacheFile/InitCacheLoaderModule)存在但本页未展开 src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.h:139-144。
5.3 fwBoxStreamer:按位置请求的“盒流送器”#
fwBoxStreamer 为一个流式模块的每个槽保存一个 AABB 与 16 位标志(低 6 位状态、高位资产类型)src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.h:27、src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.h:93-94,并用 MNBVHSimpleInterface 树加速相交查询 src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.h:242。资产类型:静态碰撞(mover/weapons/material)、MAPDATA(HIGH/HIGH_CRITICAL/MEDIUM/LOW)、METADATA src/dev_ng/rage/framework/src/fwscene/stores/boxstreamerassets.h:10-42。状态位 src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.h:191-210:
| 位 | 含义 |
|---|---|
FLAG_REQ_CURR / FLAG_REQ_PREV | 本帧/上帧被需要 |
FLAG_IGNORE | 只能手动加载/卸载(脚本 IMAP) |
FLAG_CULL | 被剔除盒剔除 |
FLAG_PIN_VOL / FLAG_PIN_USR | 被流式体积/用户钉住,不可剔除 |
RequestAboutPos(primarySearch) 三阶段 src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.cpp:274-325:
- presearch:把上帧需要的槽改为
REQ_PREV,清空新列表;对“脏列表”(本帧被手动删除)清REQ_MASK。 - search:
GenerateSearchList= 主搜索 +(可选)次要位置 + 游戏接口GetGameSpecificSearches(脚本实体、流式体积等)src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.cpp:498-514;每个搜索SetRequired做盒/线相交并对CanStream的槽置REQ_CURR并入新列表(带相机方向的搜索剔除身后的槽)src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.cpp:520-589。 - postsearch
RequestRequired:新需要且未加载 →RequestObject(…, STRFLAG_DONTDELETE | (在优先球内 ? STRFLAG_PRIORITY_LOAD : 0));已加载但上帧不需要 → 只SetRequiredFlag(DONTDELETE);上帧需要本帧不需要 →ClearRequiredFlag(DONTDELETE)src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.cpp:628-672。然后交换双缓冲列表src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.h:29-48。
即:box streamer 不直接卸载,只通过 DONTDELETE 的置/清把“是否可被流式引擎回收”交给流式引擎(见 streaming-engine.md);IMAP 的实际卸载由 fwMapDataStore::RemoveUnrequired 驱动。fwBoxStreamerVariable 为每槽额外维护“膨胀后的流式范围”及第二棵树 src/dev_ng/rage/framework/src/fwscene/stores/boxstreamer.h:297-317。
5.4 其余存储一览#
所有存储都是 fwAssetStore/fwAssetRscStore 的实例或派生(机制见 streaming-engine.md),构造参数为(模块名、文件 ID、size、elementsPerPage、[requiresTempMemory]、canDefragment、资源版本)src/dev_ng/rage/framework/src/fwtl/assetstore.h:175-181、src/dev_ng/rage/framework/src/fwtl/assetstore.h:350-356。注意:所有框架存储的 size 都是 CONFIGURED_FROM_FILE(=1)src/dev_ng/rage/framework/src/fwsys/config.h:336,真实槽数由 FinalizeSize() 以 atStringHash(模块名) 查 fwConfigManager::GetSizeOfPool 得到 src/dev_ng/rage/framework/src/fwtl/assetstore.h:457-474;下表第三列的数字是 elementsPerPage,在 USE_PAGED_POOLS_FOR_STREAMING=0(当前配置)时被构造函数直接丢弃 src/dev_ng/rage/framework/src/streaming/streamingdefs.h:34、src/dev_ng/rage/framework/src/streaming/streamingmodule.h:105-111,不是默认容量:
| 存储 | 模块名 | elementsPerPage | 证据 |
|---|---|---|---|
fwDrawableStore (g_DrawableStore) | DrawableStore | 2048 | src/dev_ng/rage/framework/src/fwscene/stores/drawablestore.cpp:28-37 |
fwDwdStore | DwdStore | 512 | src/dev_ng/rage/framework/src/fwscene/stores/dwdstore.cpp:26-33 |
fwFragmentStore | FragmentStore | 2048 | src/dev_ng/rage/framework/src/fwscene/stores/fragmentstore.cpp:32-39 |
fwTxdStore (g_TxdStore) | TxdStore | 2048 | src/dev_ng/rage/framework/src/fwscene/stores/txdstore.cpp:39-41 |
fwClipDictionaryStore | AnimStore | 1170 | src/dev_ng/rage/framework/src/fwscene/stores/clipdictionarystore.cpp:98 |
fwClothStore | ClothStore | 256 | src/dev_ng/rage/framework/src/fwscene/stores/clothstore.cpp:20 |
crFrameFilterDictionaryStore | FrameFilterStore | 292 | src/dev_ng/rage/framework/src/fwscene/stores/framefilterdictionarystore.cpp:21 |
g_ExpressionDictionaryStore | ExprDictStore | 292 | src/dev_ng/rage/framework/src/fwscene/stores/expressionsdictionarystore.cpp:15-18 |
fwMapTypesStore | MapTypesStore | 256 | src/dev_ng/rage/framework/src/fwscene/stores/maptypesstore.cpp:37 |
fwMapDataStore | MapDataStore | 455 | src/dev_ng/rage/framework/src/fwscene/stores/mapdatastore.cpp:81 |
| NetworkDefStore / PoseMatcherStore | 同名 | 292 / 292 | src/dev_ng/rage/framework/src/fwscene/stores/networkdefstore.cpp:24、src/dev_ng/rage/framework/src/fwscene/stores/posematcherstore.cpp:22 |
| StaticBounds | StaticBounds | 455 | src/dev_ng/rage/framework/src/fwscene/stores/staticboundsstore.cpp:112 |
| TextStore | TextStore | 1024 | src/dev_ng/rage/framework/src/fwscene/stores/textStore.cpp:20 |
g_BlendShapeStore | BlendShapeStore | 只传了 5 个实参,比 fwAssetRscStore 构造函数少一个;整段受 ENABLE_BLENDSHAPES(= __TOOL || !HACK_GTA4)保护 src/dev_ng/rage/base/src/grblendshapes/blendshapes_config.h:9,游戏 forceinclude 定义 HACK_GTA4 1 src/dev_ng/rage/base/src/forceinclude/win64_beta.h:515,故游戏构建不编译(推断:开启时会编译失败) | src/dev_ng/rage/framework/src/fwscene/stores/blendshapestore.cpp:18-22 |
其他辅助:fwImapGroupMgr/fwImapGroupSwap 管理“从一个手动流式 IMAP 切换到另一个”(如完好→损毁版本),并发上限 7(回放构建 21),0 号留给脚本 src/dev_ng/rage/framework/src/fwscene/stores/imapgroup.h:20-35、src/dev_ng/rage/framework/src/fwscene/stores/imapgroup.h:89-104;fwStoreSlotManager 在流式系统之外管理动态添加的槽并做垃圾回收 src/dev_ng/rage/framework/src/fwscene/stores/storeslotmanager.h:1-40;fwImposedTextureDictionary 最多挂 8 张外部纹理且不拥有其生命周期 src/dev_ng/rage/framework/src/fwscene/stores/imposedTextureDictionary.h:29-35;fwPsoStoreLoader 负责 PSO 原位/非原位加载 src/dev_ng/rage/framework/src/fwscene/stores/psostore.h:20-40(PSO 格式见 ../02-rage-core/parser-and-metadata.md)。fwscene/Ide.h 注释说明二进制 IDE 格式当前不用 src/dev_ng/rage/framework/src/fwscene/Ide.h:10-11;texLod.h 的 fwAssetLocation 把“存储类型 8 位 + 槽 24 位”打包进一个 u32 src/dev_ng/rage/framework/src/fwscene/texLod.h:21-35。fwScene/fwSceneInterface 只是让游戏注入 InitPools/ShutdownPools 的薄接口 src/dev_ng/rage/framework/src/fwscene/scene.h:13-31。
6. 约束与边界行为汇总#
| 主题 | 约束/边界 | 证据 |
|---|---|---|
| 世界修改时机 | 扫描/搜索作业运行期间修改场景图会 fatal assert;锁只是计数器,非 assert 构建无保护 | src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.h:15、src/dev_ng/rage/framework/src/fwscene/world/SceneGraph.h:128-131 |
| 描述符指针稳定性 | 每帧 PreScan defrag 会移动描述符,外部不应长期保存 fwEntityDesc* | src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.h:90-112 |
| 容量失败 | 描述符池满/碎片 → Quitf;实体作业超 2048(旧主机 500)只 assert;PVS/结果块越界 FatalAssert | src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:19-70、src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:354、src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.cpp:228-251 |
| 动态实体上限 | 外景动态固定容器 2560 槽,满则 AppendEntity 返回 NULL(仅 assert) | src/dev_ng/rage/framework/src/fwscene/world/ExteriorSceneGraphNode.cpp:12、src/dev_ng/rage/framework/src/fwscene/world/EntityContainer.cpp:571-573 |
| 遍历深度 | 访问器栈与门户递归都限 24 层 | src/dev_ng/rage/framework/src/fwscene/world/SceneGraphVisitor.h:21-23、src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:550-553 |
| cull shape 数 | ≤24 | src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:206 |
| 扫描结果读取线程 | fwScan::GetScanResults() 只允许更新线程 | src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:214-217 |
| 搜索并发 | 默认搜索对象是单例,不可重入 | src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:47、src/dev_ng/rage/framework/src/fwscene/search/Search.cpp:240-243 |
| 搜索容量 | 默认 5120,超出静默截断(assert 告警) | src/dev_ng/rage/framework/src/fwscene/search/SearchEntities.cpp:146-157 |
| 世界边界 | X/Y ±16000、Z −1700..2700 | src/dev_ng/rage/framework/src/fwscene/world/WorldLimits.h:16-21 |
已知源码缺陷(均为观察到的代码事实;对实际表现的影响为推断):
bIsVisibleInGBuf逻辑与 → 所有实体扫描作业 Critical 优先级src/dev_ng/rage/framework/src/fwscene/scan/ScanNodes.cpp:331。_GTA5_VIS_PHASE_MASK_REFLECTIONS用索引拼掩码,仅影响 BANK 调试src/dev_ng/rage/framework/src/fwscene/world/EntityDesc.h:52-54。fwStreamedSceneGraphNode::RemoveStreamedContainer删除首元素无效、未命中会空指针;无调用者src/dev_ng/rage/framework/src/fwscene/world/StreamedSceneGraphNode.cpp:37-57。SCAN_ENTITIES_DONT_RUN_ON_PPU只被设置从未读取src/dev_ng/rage/framework/src/fwscene/scan/ScanEntities.h:83。- PC 上 cull shape 排序比较器不满足
qsort的三值约定(注释自承)src/dev_ng/rage/framework/src/fwscene/scan/Scan.cpp:183-189。
7. 未决问题 / 证据不足#
fwScanEntities::ProcessEntityDescsSoA的矢量化路径、IsEntityClippedShadows/Reflections、TestOcclusion的 HiZ 细节、ScanCascadeShadows.cpp的阴影视锥构造本页只给出入口,未逐行核对。- 门户遍历中镜子/镜面地板/水面门户对
fwScanResults的更新(ScanNodes.cpp713 行之后)未展开。 fwDescPool的 defrag 是否会移动固定容器(动态实体组件缓存的描述符指针是否始终有效)未在DefragPool实现中核实;相关池实现见 framework-systems.md。FW_INSTANTIATE_CLASS_POOL[_SPILLOVER]为场景图类生成的InitPool是否在别处被调用、从而与fwSceneGraph::InitPools重复,未追踪。fwMapDataStore的缓存加载(fwMapDataCacheEntry)、SetBoxStreamerSlot、ProcessPostLaunchHacks,以及fwBoxStreamer::CanStream/CanRemoveAsset的完整条件未展开。fwscene/lod(fwLodAttach、fwContainerLodMap)只在 entity-and-archetype.md 与 ../05-world/map-data-and-lod.md 中涉及,本页未单独分析。