CD Code Digest Hub /GTA V · v2

Drawlist 与渲染线程交接:命令缓冲、drawlist 生命周期、fence 与引用计数

71 分钟阅读 12 个章节 4 张图

GTA V 的主线程不直接调用图形 API。它在 CGame::Render 里把整帧渲染“录制”成一串 drawlist:每个 drawlist 是命令缓冲(gDCBuffer)中的一段连续命令(dlCmd*),由渲染阶段按顺序生成,生成完一个就推入消息队列;渲染线程从队列中逐个弹出并解释执行。本页解释这套机制的数据结构(命令、页、共享数据)、生命周期(构建 → 执行 → 回收)、帧间多缓冲(fence 索引 FENCE_COUNT = 4、读缓冲地址历史 3 帧)、资源引用计数如何保证 GPU 用完前不释放,以及主机上的多子渲染线程(MTR)。

背景与相关页面#

相关源码文件#

文件内容
src/dev_ng/rage/framework/src/fwdrawlist/drawlist.{h,cpp}dlCmdBase、指令 ID 表、DLC/DLC_Add 宏、DrawListAddress、dlDrawListPage、dlDrawCommandBuffer(gDCBuffer)、共享数据、通用回调命令、系统命令实现
src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.{h,cpp}dlDrawListInfo、DrawListQ、dlDrawListMgr(NewDrawList/EndDrawList/ExecuteAndRemoveAll/RemoveAll、fence、引用、子渲染线程)
src/dev_ng/rage/framework/src/fwrenderer/renderthread.{h,cpp,dtx}渲染线程主循环、同步、GPU_IdleSection;.dtx 是设计文档
src/dev_ng/game/renderer/DrawLists/drawListMgr.{h,cpp}、drawListMgr_config.heGTADrawListType、CDrawListMgr(每类 drawlist 的名称/CPU 掩码/LOD 偏移/预算)
src/dev_ng/game/renderer/DrawLists/drawList.{h,cpp}eGtaInstructionId、CDrawCommandBuffer、实体/ped/车辆/碎片 DC、原型批、着色效果拷贝
src/dev_ng/game/renderer/DrawLists/DrawableDataStructs.{h,cpp}、drawListCommands_Entities.cpp、CopyOffMatrixSet.cpp实体绘制数据快照(CEntityDrawData 等)、骨骼矩阵拷贝依赖任务
src/dev_ng/game/system/system.cppCSystem::AddDrawList_BeginRender/EndRender(帧首尾 drawlist、dlCmdInsertEndFence)
src/dev_ng/rage/base/src/system/messagequeue.hsysMessageQueue(DrawListQ 的实现)

1. 总体模型#

图证据:NewDrawList src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:793-855;EndDrawList/EndCurrentDrawList src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:864-932;TerminateDrawLists/BeginDrawListCreation/EndDrawListCreation src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:934-979;ExecuteAndRemoveAll 主循环 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1527-1891、结束时 UnlockReadBuffer src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1912;DrawListQ 为 sysMessageQueue<dlDrawListInfo*, MAX_DRAWLISTS>、MAX_DRAWLISTS = 128 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:44、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:101。

核心特征:流水式而非整帧交付。每个 drawlist 一旦 EndDrawList 就立即 Push 进队列(注释“As soon as it's built, push it onto the queue so the render thread can go for it”)src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:901-902,渲染线程可在主线程还在构建后续阶段时就开始执行前面的 drawlist。帧结束标记是一个 NULL 指针 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:934-942。

1.1 渲染线程如何“等待” drawlist#

渲染线程默认渲染函数依次调用 DefaultPreRenderFunction → gDrawListMgr->ExecuteAndRemoveAll() → DefaultPostRenderFunction → FlipRenderBuffer → FlipRenderFenceIdx src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:926-936。ExecuteAndRemoveAll 的循环每次调用 GetRenderDrawListQ().Pop() src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1534。sysMessageQueue::Pop 先 sysIpcWaitSema(m_NonEmpty),Push 在入队后 sysIpcSignalSema(m_NonEmpty);DrawListQ 的 _blockOnFull 模板参数为默认 false,所以推入方从不阻塞,队满时 Push 返回 NULL(元素被丢弃)src/dev_ng/rage/base/src/system/messagequeue.h:27-80。这就是 threading 页 §7 所说“渲染线程在 ExecuteAndRemoveAll 中等待本帧第一个 drawlist”的具体实现:渲染线程阻塞在信号量上,直到主线程推入本帧第一个 drawlist(DL_BEGIN_DRAW)。

与帧同步的关系(与 01 页一致):主线程 Synchronise 在 WaitTillRenderDone 之后调用 SetToPreDrawListState();CGame::Render 中 CSystem::AddDrawList_BeginRender 调用 BeginDrawListCreation(断言状态为 UPDATE_PRE_DRAWLIST 或 UPDATE_INITIALIZED),AddDrawList_EndRender 调用 EndDrawListCreation,状态变为 UPDATE_WAITING_FOR_RT src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:484-515、src/dev_ng/game/system/system.cpp:1133-1163、src/dev_ng/game/system/system.cpp:1302-1314。

图证据:枚举与 SetToPreDrawListState src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:484-515;转换 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:934-960;Synchronise 调用 SetToPreDrawListState src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:377-447(01 页 §7.1 引用的同一段)。

2. 命令(dlCmd*)#

2.1 命令格式与分配#

dlCmdBase 只有一个 32 位头:m_commandSize:12(以 DRAW_COMMAND_BLOCK_SIZE = 4 字节为单位)+ m_instructionId:8(__DEV 另有 12 位 m_commandIdx)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:830-907、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:66。因此单条命令最大约 16 KB,指令 ID 最多 256(JUMPTABLE_SIZE = 256);数据块上限 MAX_DATA_BLOCK_BYTES 由同样的 12 位推导 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:73、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:83-86。

写入方式是“placement new 到命令缓冲当前写指针”:DLC(type, ctorArgs) 展开为 PAD_DLC<type>(); ADD_DLC(new type ctorArgs) src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:91-96;dlCmdBase::operator new 断言调用线程是 update 线程,空间不足时 MoveToNewPage(DPT_SIMPLE_RING_BUFFER),然后返回当前写地址并前移写指针 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2586-2623。任何非 update 线程(包括依赖工作线程)都不能直接 DLC;需要在别处生成的数据通过 sysDependency 写入预先分配的数据块(§6.3)。命令的构造函数在主线程运行(可在此做快照/加引用),Execute 在渲染线程运行——这是理解所有 dlCmd* 的关键。

执行方式是跳转表:dlDrawCommandBuffer::m_JumpTable[256] 存放每个指令 ID 的 ExecuteStatic,由 DLC_REGISTER(cmdType) 注册(bank 版本额外注册调试信息回调与名称)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:98-104、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:702。ExecuteListAtAddr 从 drawlist 起始地址逐条解析:读命令大小、先前移读指针再执行(以便命令在执行中读取紧随其后的数据块)、查跳转表调用、遇到 DC_EndDrawList 结束;结束时断言没有泄漏的强制 technique 组与强制着色器 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:986-1117。

2.2 指令 ID 空间#

范围所属例子证据
1–127RAGE 框架系统命令DC_NewDrawList=1、DC_EndDrawList=2、DC_DataBlock=3、DC_BeginDraw=4、DC_EndDraw=5、DC_SetGlobalVariableF=10、DC_LightStateSetEnabled=40、DC_InsertEndFence=41、DC_CB_GenericNoArgs=50、DC_CB_GenericArgs=51、DC_SwitchPage=80、DC_SetRasterizerState=90、DC_NOP=127src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:128-252
128–235游戏命令(eGtaInstructionId)DC_DrawEntity=128、DC_DrawEntityFM=129、DC_DrawSkinnedEntity=130、DC_DrawPedBIG=131、DC_DrawStreamPed=132、DC_DrawFrag=134、DC_DrawPrototypeBatch=136、DC_CustomShaderEffect=137、DC_BeginRender=179、DC_RenderPhasesDrawInit=181、DC_EndRender=189、DC_ProcessNonDepthFX=192、DC_SetupTimeCycleFrameInfo=206、DC_SetupLightsFrameInfo=208、DC_DrawEntityInstanced=227、DC_DrawGrassBatch=228src/dev_ng/game/renderer/DrawLists/drawList.h:68-189

注:框架注释写“NY project adds commands starting at this point”并把 DC_NumBaseDrawCommands 设为 240,但游戏实际从 128 开始编号;两者不冲突只因框架命令都 ≤ 127 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:250-251。

注册点:框架系统命令在 dlDrawCommandBuffer::Initialise src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:341-400;游戏的 CDrawCommandBuffer::Initialise 注册实体/ped/车辆/碎片/原型批/着色效果/小地图/精灵/文本/粒子/玻璃等 DC,并调用各子系统的 InitDLCCommands(CRenderListBuilder、CViewportManager、Water、CRenderPhaseDrawSceneInterface、CTimeCycle、Lights、CVisualEffects、CPickupManager、CPedDamageManager)src/dev_ng/game/renderer/DrawLists/drawList.cpp:126-204。CDrawListMgr::Initialise 先 gDCBuffer = rage_new CDrawCommandBuffer() 再 dlDrawListMgr::Init(DL_MAX_TYPES) src/dev_ng/game/renderer/DrawLists/drawListMgr.cpp:116-120。

2.3 通用回调命令 DLC_Add#

游戏代码中绝大多数渲染线程工作不是专门的命令类,而是 DLC_Add(func, args...):它生成 T_CB_Generic_NArgs<T_func, args...>(INSTRUCTION_ID = DC_CB_GenericArgs),把函数指针与按值拷贝的参数放进命令体,执行时调用 m_Func(m_arg1, …);支持 0–9 个参数;非 final 版本保存名称字符串(#_cmd)供调试 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:1700-1800、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:2030-2048。由按值拷贝可推出约束:参数在录制时求值,必须是可平凡拷贝的值;传指针时所指数据在渲染线程执行时是否仍有效由调用者保证(推断:没有运行时检查)。

3. 命令缓冲内存:页、环形缓冲与多缓冲历史#

3.1 DrawListAddress 与页#

命令缓冲不是一块连续内存,而是一组页(dlDrawListPage),从流式堆分配;地址用 32 位 DrawListAddress 表示:高 8 位页索引、低 24 位页内偏移,0xffffffff 为空 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:295-334、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:350-406。

常量值证据
MAX_DRAWLIST_PAGESPC/Durango/Orbis 128,其他 48src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:58
DEFAULT_DRAWLIST_PAGE_SIZE / DEFAULT_MIN_DRAWLIST_PAGE_SIZE128 KB / 16 KBsrc/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:69-70
MAX_EMERGENCY_PAGES2src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:68
MAX_DRAWLIST_ADDR_HISTORY3(注释:3 支持三缓冲页)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:77-79
MAX_DRAWLIST_PAGE_AGE25(帧)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:82、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:329
MAX_ARCHETYPE_REFS / MAX_HD_ARCHETYPE_REFS / MAX_CSET_REFS新平台 8192 / 600 / 8192;旧平台 2800 / 160 / 2350src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:59-61

3.2 页类型(数据寿命)#

eDrawListPageTypem_Lifetime(回收时使用的历史索引)用途
DPT_SIMPLE_RING_BUFFER0命令本身与内联数据块;只由 update 线程写
DPT_LIFETIME_RENDER_THREAD(_PHYSICAL)0需要存活到渲染线程用完的数据(如每帧光照快照 LightingData)
DPT_LIFETIME_GPU(_PHYSICAL)2需要存活到 GPU 用完的数据(多保留两帧)

证据:src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:256-279、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:126-137。ONE_STREAMING_HEAP 时物理/虚拟两类合并。

3.3 分配、换页与阻塞#

  • 换页:MoveToNewPage 申请新页;对环形缓冲类型,在当前页末尾就地构造 dlCmdSwitchPage(不能走 operator new,否则会递归触发换页),执行时把读指针跳到新页;前页的 m_NextPage 链到新页 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1647-1695。
  • 申请页:AllocateDrawListPage 优先复用“空闲(idle)”列表中物理属性匹配的页;否则从空槽分配,从 128 KB 起逐次减半直到 16 KB;内存不足时动用应急页;仍失败则 sysIpcSleep(1) 循环等待渲染线程释放(第 80/160 次尝试时转储页信息/内存并断言)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:681-859。这意味着主线程构建 drawlist 时可能阻塞等待渲染线程回收页。
  • 数据块:AddDataBlock 先对齐,再写一个 dlCmdDataBlock 头(执行时为空操作),后跟原始数据,返回数据指针与其 DrawListAddress;放不下时换页 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1750-1789、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2701-2709。

3.4 帧翻转与回收#

图证据:FlipBufferIdx src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2456-2464;FlipBuffers(注释“The render thread may not be executing draw lists while this function is being called!”)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1936-2055;UnlockReadBuffer src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1794-1868;MarkPagesForRecycling、AgeAndDeleteOldPages src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1870-1934。游戏的 CDrawCommandBuffer::FlipBuffers 额外清空 ScriptIM(脚本立即模式绘制)src/dev_ng/game/renderer/DrawLists/drawList.cpp:231-239。

含义:环形缓冲(寿命 0)中某帧的页在渲染线程执行完该帧、UnlockReadBuffer 之后即可复用;DPT_LIFETIME_GPU(寿命 2)的页要再经过两次历史左移才回收,覆盖“GPU 落后 CPU 最多两帧”的窗口。关于 FlipBuffers 的注释约束:它在 EndDrawListCreation 中、推入 NULL 之后调用,此时渲染线程可能仍在执行本帧前面的 drawlist;本次未找到显式互斥。FlipBuffers 改写的是 update 侧写入起点、读缓冲历史与共享数据表,渲染线程读取的是 drawlist 中保存的地址,这表明注释描述的约束可能比实际需要更严格(推断)。

4. 共享数据(dlSharedDataInfo):同一帧内去重#

同一个对象(着色效果、骨骼矩阵集、实体绘制数据)在一帧内常被多个阶段绘制(GBuffer、阴影、反射…)。为避免重复拷贝,每个可共享对象内嵌一个 dlSharedDataInfo {DrawListAddress m_Offset; u32 m_Timestamp;} src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:410-428:

  • LookupSharedData 仅当 m_Timestamp == gDCBuffer->m_Timestamp 时返回已有地址,否则返回空 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1571-1581;
  • 首次使用时 AddDataBlock 拷贝数据,再 AllocateSharedData 记下地址与当前时间戳 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1605-1621;
  • 每帧 FlipBuffers 末尾 m_Timestamp++,所有旧记录自动失效 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2054。

共享内存类型 eSharedMemType:DL_MEMTYPE_SHADER、DL_MEMTYPE_MATRIXSET、DL_MEMTYPE_COMMAND_HELPER、DL_MEMTYPE_ENTITY、DL_MEMTYPE_CUSTOM,上限 MAX_DL_SHARED_MEM_TYPES = 5 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:113-125、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.h:75。无法内嵌 dlSharedDataInfo 的对象用 AddSharedDataBlock,按指针 ID 线性查表(dlSharedMemData::m_SharedDataMap)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1713-1737;源码注释指出该函数两条返回路径的语义与文档不一致 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:1733-1736。

游戏中的使用:

场景实现证据
实体绘制数据CopyOffEntity<EntityDrawData>:用 draw handler 内嵌的 dlSharedDataInfo 查找/分配 DL_MEMTYPE_ENTITY 块,首次时 Init(pEntity) 并 AddArchetypeReferencesrc/dev_ng/game/renderer/DrawLists/drawListCommands_Entities.cpp:104-130
自定义着色效果CopyOffShader:DL_MEMTYPE_SHADER;双缓冲时放进 DPT_LIFETIME_RENDER_THREAD 页(PS3 为 DPT_LIFETIME_GPU,供 SPU/EDGE 稍后读取),否则内联数据块;对效果类型加引用并登记到 drawlist 管理器src/dev_ng/game/renderer/DrawLists/drawList.cpp:241-283
骨骼矩阵集DL_MEMTYPE_MATRIXSET,见 §6.3src/dev_ng/game/renderer/DrawLists/drawListCommands_Entities.cpp:430-499

5. drawlist 元数据与渲染线程执行#

5.1 dlDrawListInfo 与类型表#

dlDrawListInfo 记录 drawlist 起始地址 m_offset、终止命令索引、类型、状态(DLS_BUILDING → DLS_BUILT → DLS_EXECUTING → DLS_EXECUTED)、唯一 ID、依赖信息索引、子类数据与“强制清理回调”链表 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:36-99。对象来自 atPool<dlDrawListInfo>(容量 MAX_DRAWLISTS = 128),由 m_DrawListPoolCs 临界区保护(主线程分配、渲染线程释放)src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:309-329、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:809-811、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1441-1451。pCurrBuildingList、pCurrExecutingList 是线程局部变量 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:103-104。NewDrawList 不允许嵌套(断言)且只允许在创建窗口内调用 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:799-808;每个新 drawlist 的第一条命令是 dlComputeShaderBatch src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:851-852。

游戏的 drawlist 类型 eGTADrawListType:DL_RENDERPHASE … DL_RENDERPHASE_LENSDISTORTION、DL_BEGIN_DRAW、DL_END_DRAW,以及只在拆分开关打开时存在的子类型(GBuffer 48 个 + 环境灯,抛物面阴影 8 个,级联阴影 4 个 + reveal,DrawScene 24 个 alpha 子列表等)src/dev_ng/game/renderer/DrawLists/drawListMgr.h:30-142。CDrawListMgr::Initialise 用 SetDrawListInfo(type, 名称, CPU 掩码, LOD 偏移, GPU 计费调试, 更新/渲染/GPU 预算, 计时条) 登记每一类的属性:例如水反射的 LOD 偏移是 LODFLAG_SHIFT_WATER、反射图是 LODFLAG_SHIFT_REFLECTION、阴影是 LODFLAG_SHIFT_SHADOW,DL_BEGIN_DRAW/DL_END_DRAW 是 CPU_SYNC_MAIN src/dev_ng/game/renderer/DrawLists/drawListMgr.cpp:158-266。LOD 偏移在 dlCmdNewDrawList::Execute 时写入线程变量,供 drawable 选择该阶段的 LOD src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2749-2764。CDrawListMgr 的 IsBuilding*/IsExecuting*DrawList 系列分别断言调用线程为 update/render src/dev_ng/game/renderer/DrawLists/drawListMgr.h:282-330。

5.2 ExecuteAndRemoveAll(单渲染线程路径,PC)#

对每个弹出的 drawlist:非 PC 平台调用 PrimeEndFenceHandle(把本帧 fence 状态从 FENCE_NONE 置为 FENCE_UNSET);状态置 DLS_EXECUTING;若有依赖信息先 JoinDrawlistDependencies;重置状态块、矩阵集与 effect 缓存及 LOD 上限;gDCBuffer->ExecuteListAtAddr;CleanupDrawList 归还池 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1554-1611、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1832-1891、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2417-2428。遇到 NULL 后:非 PC 检查若 fence 仍为 FENCE_UNSET(执行了 drawlist 却没插 fence),有布料引用时断言,并置为 FENCE_NONE;最后 UnlockReadBuffer src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1892-1912。bank 版本在入口处理着色器重载请求 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1485-1502。

RemoveAll 是“丢弃路径”(自定义渲染函数、失焦、设备丢失时):弹出所有 drawlist,执行它们的强制清理回调、join 依赖、归还池,但不执行命令,最后同样 UnlockReadBuffer src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1929-1963。调试绘制批处理就是借助强制清理回调保证 drawlist 被丢弃时也能释放 src/dev_ng/rage/framework/src/fwrenderer/RenderPhaseBase.cpp:153-169。

5.3 每帧的首尾 drawlist#

drawlist内容证据
DL_BEGIN_DRAW(PC D3D11)开始缓冲更新批;CRenderTargets::ResetRenderThreadInfo;timecycle/灯光/级联阴影/视效的帧信息快照;粒子 BeginFrame;CSetDrawableLODCalcPos(相机位置);dlCmdBeginRender;抛物面阴影缓存更新;可选异步清阴影图src/dev_ng/game/system/system.cpp:1133-1163
DL_RENDERPHASE_PREAMBLEdlCmdRenderPhasesDrawInitsrc/dev_ng/game/camera/viewports/ViewportManager.cpp:1219-1221
各阶段 drawlist见 render-pipeline-and-phases.md
DL_END_DRAW(PC D3D11)结束缓冲更新批;dlCmdInsertEndFence;dlCmdEndRender;清除 timecycle/灯光/视效帧信息src/dev_ng/game/system/system.cpp:1302-1314

帧信息快照是另一种交接方式:例如 dlCmdSetupLightsFrameInfo 在构造时(主线程)从 DPT_LIFETIME_RENDER_THREAD 页分配 LightingData 并 memcpy 当前灯光数据,执行时(渲染线程)把 Lights::m_currFrameInfo 指向这份拷贝;DL_END_DRAW 中的 clear 命令将其置空 src/dev_ng/game/renderer/Lights/lights.cpp:445-486、src/dev_ng/game/renderer/Lights/lights.cpp:4613-4623。timecycle 以同样方式(dlCmdSetupTimeCycleFrameInfo)传递 src/dev_ng/game/timecycle/TimeCycle.cpp:4510-4521。与“在安全区翻转双缓冲”(如 PostFX 参数块,见 render-pipeline-and-phases.md)相比,快照随 drawlist 一起流动,渲染线程读到的始终是录制该帧时的数据。

6. 实体绘制数据:从 draw handler 到 DC#

6.1 基本实体#

CDrawEntityDC 只含一个 DrawListAddress m_dataAddress(+ dev 调试信息),指向共享的 CEntityDrawData;执行时 GetDataBlock 解析地址并 Draw() src/dev_ng/game/renderer/DrawLists/drawList.h:245-264。CEntityDrawData 是实体在录制时刻的紧凑快照:两个 __vector4(拼成 fwTransform)、rmcDrawable*、16 位模型索引、8 位淡出 alpha、自然/人工环境光、材质 ID、15 位“各阶段 LOD”、上一 LOD 索引、内部标志等 src/dev_ng/game/renderer/DrawLists/DrawableDataStructs.h:93-134。渲染线程绘制时读取当前 bucket/render mode/bucket mask 与“正在执行哪类 drawlist”,决定 alpha/stipple、是否写深度、树的两 pass 渲染,设置 CShaderLib 全局(环境光、alpha、材质 ID、内部标志)后调用 drawable 绘制 src/dev_ng/game/renderer/DrawLists/DrawableDataStructs.cpp:401-480。满变换实体用 CDrawEntityFmDC/CEntityDrawDataFm src/dev_ng/game/renderer/DrawLists/drawList.h:268-286。

其他数据结构:CEntityInstancedDrawData、CGrassBatchDrawData、ped 的 CEntityDrawDataStreamPed(WithProps)、CEntityDrawDataPedBIG(WithProps)、CEntityDrawDataDetachedPedProp、车辆 CEntityDrawDataVehicleVar、碎片 CEntityDrawDataFrag/FragType/FragTypeVehicle、碎玻璃、蒙皮 CEntityDrawDataSkinned src/dev_ng/game/renderer/DrawLists/DrawableDataStructs.h:220-686。对应命令类见 src/dev_ng/game/renderer/DrawLists/drawList.h:288-630。

6.2 原型批(prototype batch)#

IDrawListPrototype 接口:AddDataForEntity(把一个实体的数据写进页)、Draw(batchSize, data)、SizeOfElement src/dev_ng/game/renderer/DrawLists/drawList.h:581-606。CDrawListPrototypeManager 维护当前原型与一块从 gDCBuffer->AddDataBlock 取得的页:切换原型或页满时 Flush 发出一条 CDrawPrototypeBatchDC(prototype, batchSize, address);新的一帧(gDCBuffer->GetTimeStamp() 变化)必须重新分配页 src/dev_ng/game/renderer/Entities/EntityDrawHandler.h:20-163、src/dev_ng/game/renderer/Entities/EntityDrawHandler.cpp:691-728。走非原型路径的 draw handler(碎片、蒙皮、可弯曲)在 AddToDrawList 开头先 CDrawListPrototypeManager::Flush(),以保持命令顺序 src/dev_ng/game/renderer/Entities/EntityDrawHandler.h:235-246、src/dev_ng/game/renderer/Entities/DynamicEntityDrawHandler.h:16-27。原型选择逻辑见 render-pipeline-and-phases.md §5.5。

6.3 骨骼矩阵:drawlist 依赖任务#

蒙皮实体的矩阵集在录制时只预留:AddDataBlock(NULL, grmMatrixSet::ComputeSize(n)) 得到目标地址并登记为 DL_MEMTYPE_MATRIXSET 共享数据,然后 gDrawListMgr->CreateDrawlistDependency() 初始化一个 CopyOffMatrixSetSPU_Dependency 任务(输入:对象矩阵、累计逆关节、可选 LOD 骨骼映射;输出:目标 Matrix43 数组);注释说明 Insert 被推迟到 drawlist 结束时批量提交 src/dev_ng/game/renderer/DrawLists/drawListCommands_Entities.cpp:449-496、src/dev_ng/game/renderer/DrawLists/CopyOffMatrixSet.cpp:83-98。EndDrawList 调用 SubmitPendingDependencies(),若提交了任务就给该 drawlist 分配依赖信息索引 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:893-899;渲染线程执行该 drawlist 前 JoinDrawlistDependencies 等待这些任务 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1606-1611;EndDrawListCreation 先 JoinAllDrawlistDependencies src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:968。依赖池容量 MAX_DEPENDENCY_COUNT = 512,依赖信息 MAX_DRAWLIST_DEPENDENCY_INFOS = 40;头文件警告这些 sysDependency 只能单独 kick、不能链接成图,提交后不得再引用(会被回收)src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:456-473、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:580-597、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:46。

7. GPU fence 与引用计数(FENCE_COUNT = 4)#

drawlist 中引用的资源(纹理字典、drawable、fragment、archetype、HD archetype、自定义着色效果类型、布料)必须在渲染线程/GPU 用完之前保持存活。机制:

  1. 加引用(主线程,录制时):如 AddTxdReference 对 g_TxdStore 加 REF_RENDER,AddArchetypeReference 对 archetype AddRef(),然后 AddRefCountedModuleIndex(index, module) 记入当前流式索引缓存(m_aStrIndicesCache[m_streamingIndicesBufferIndex])src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1968-1996、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2093-2128。
  2. 插 fence(渲染线程,非 PC):DL_END_DRAW 中的 dlCmdInsertEndFence::Execute 只在 !RSG_PC 时调用 InsertEndFence()(注释“don't want to do this if not actually using a render thread”)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:3473-3482;InsertEndFence 对 m_endFenceHandle[m_fenceRenderIdx] 标记 CPU 待定并让 GPU 完成时标记,状态 FENCE_UNSET → FENCE_SET src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2402-2412。若渲染线程本帧没画(自定义渲染函数),设为 FENCE_SKIPPED src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2349-2355。
  3. 翻转索引:初值 m_fenceUpdateIdx = 0、m_fenceRenderIdx = 3 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:239-241;渲染线程每帧末 FlipRenderFenceIdx(默认渲染函数、自定义函数与丢弃路径都会调用);主线程在 GPU_IdleSection 开头 FlipUpdateFenceIdx(同时推进流式索引缓存索引);两者对 FENCE_COUNT = 4 取模 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2466-2477、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:51-52、src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:936。
  4. 等待并释放(主线程):GPU_IdleSection 在非 PC 平台取 GetEndFenceHandle() 并 CpuWaitOnFence,再 ClearEndFenceHandle;所有平台随后 RemoveAllRefs():对当前 update 索引的布料引用减计数、对当前流式索引缓存中每个条目 RemoveRef(REF_RENDER)、移除 HD archetype 引用、释放着色效果类型引用,最后断言 update 与 render 的 fence 索引不相等(否则“objects being released too early”)src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:449-496、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:2488-2560。

引用的生命周期因此是:在索引 i 下录制的引用,要等主线程的 update 索引绕回 i(即再经过 4 次 GPU_IdleSection)时才释放;主机上释放前还要等待索引 i 对应的 GPU fence。

平台渲染线程插 fence主线程等待 GPU fence引用释放证据
PC否否按 4 槽轮换释放(不看 GPU 进度)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:3478-3481、src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:456-482
主机是是等 fence 后释放同上

PC 上不等待 fence 的理由在源码中没有说明;这表明 PC 依赖 D3D11 驱动自身对资源的生命周期跟踪与帧排队上限(推断:未找到相应注释或 MaxFrameLatency 设置的证据,属于 03 页 grcore 范围)。流式索引缓存数 STREAMING_INDICES_BUFFER_COUNT 在 PS3 为 FENCE_COUNT - 1 = 3,其他平台为 4 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:52。

文档与代码的差异:renderthread.dtx 写的是“render thread holds a triple-buffered list of GPU fence handles”,并以 fence 0/1/2 轮转、“the one that was created two frames ago”描述 src/dev_ng/rage/framework/src/fwrenderer/renderthread.dtx:150-197,头文件注释也写“two frames ago”src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:282-283;但代码 FENCE_COUNT = 4。本页没有逐帧建模两个索引的相对相位,因此不给出“GPU 允许落后几帧”的精确值;修改时请以代码为准。

8. 主机多渲染线程(MTR)#

仅当 MULTIPLE_RENDER_THREADS != 0(Durango/Orbis 为 4)编译。

图证据:子线程首次调用时创建,名称 Subrender:%d、优先级 PRIO_IDLE、不绑核(注释:允许漫游到有空闲的核)src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1455-1478;分派与汇合 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1613-1831;子线程循环 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1336-1438。

要点:

  • 每个 drawlist 类型有 CPU 掩码:SUBCPUS_SYNC_MAIN = 0x80 表示同步点(主渲染线程先等待之前全部完成);SUBCPUS_MAIN = 0x00(仅当 MULTIPLE_RENDER_THREADS_ALLOW_DRAW_LISTS_ON_RENDER_THREAD:PC/Durango 为 1、Orbis 为 0)表示在主渲染线程执行 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:151-166、src/dev_ng/rage/base/src/grcore/effect_mrt_config.h:26-28。游戏把光照 drawlist 与 GBuffer 的环境灯子列表绑在 CPU_LIGHTS = CPU_1(注释:GBuffer 阶段为光照阶段输出灯光),把 PostFX/HUD 绑在 CPU_DRAWSCENE(D3D11 下为主渲染线程,因为 UAV append 计数器非线程安全)src/dev_ng/game/renderer/DrawLists/drawListMgr.cpp:127-156。
  • 一个 job 可推给多个子线程,先到者用 sysInterlockedOr(&job->grabbed, 1) 抢占 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1364-1367。子线程每个 job 前把视口置 NULL、状态块恢复默认、清矩阵集与 effect 缓存——每个 drawlist 都必须自行设置视口与状态(注释“All drawlists must ensure that they set up their own viewport”)src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1393-1405。这就是拆分阶段在每个子 drawlist 开头重发 DLCRenderPhaseDrawInit 与视口的原因(见 render-pipeline-and-phases.md §6.3)。
  • 提交顺序严格按 drawlist 推入顺序(nextCommandList 递增),子线程只是并行“录制”。GRCGFXCONTEXT_CONTROL_SUPPORTED 时用分段预算防止“后面的上下文耗尽段内存、前面的上下文阻塞”造成的死锁:为主渲染线程保留 2 段,按队列位置分配预算,另有 25% 自由竞争;源码注释记录了这一方案的演化 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1010-1064。子任务槽 SUB_JOB_LIST_SIZE = 128 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1004。
  • 线程局部状态:m_rtRenderMode/m_rtBucket/m_rtSubBucketMask、强制 technique 栈都以 DECLARE_MTR_THREAD 声明 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:564-570;LOD 上限信息按 NUMBER_OF_RENDER_THREADS + 1 份存放 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.h:365-374。

8.1 PC 是否使用 D3D11 deferred context?#

结论:不使用(在本次检索范围内未发现)。

  • grcDevice::CreateContextForThread 中有 CreateDeferredContext(1) 实现 src/dev_ng/rage/base/src/grcore/device_d3d11.cpp:7557-7600,它只由 SubRenderThread 调用 src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1344,而子渲染线程整段代码受 #if MULTIPLE_RENDER_THREADS 保护,PC 上该宏为 0 src/dev_ng/rage/base/src/grcore/effect_mrt_config.h:2-11。
  • 在 game/ 下检索 CreateContextForThread|BeginContext(|EndContext(|ExecuteCommandList|CreateDeferredContext|D3D11_DEVICE_CONTEXT_DEFERRED|ID3D11DeviceContext,命中只有设备创建/系统信息中的立即上下文、computeshader.cpp 对 GetCurrentContextEx() 的使用,以及 EntityBatch_Def.h 的注释“we only use an ID3D11Device … and not a ID3D11DeviceContext”(BackgroundScripts::EndContext 是同名无关函数)src/dev_ng/game/renderer/computeshader.cpp:447、src/dev_ng/game/scene/EntityBatch_Def.h:117。
  • PC 的跨线程资源更新改走 dlCmdGrcDeviceUpdateBuffer:主线程登记缓冲/纹理更新(AddBufferUpdate/AddTextureUpdate),在 DL_BEGIN_DRAW/DL_END_DRAW 前后由 OnBeginRender/OnEndRender 批量处理 src/dev_ng/game/system/system.cpp:1138-1140、src/dev_ng/game/system/system.cpp:1303-1305、src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:3346-3354;当待定渲染函数不是默认函数时,挂起的更新在安全区内持有 LockContext 执行(见 threading 页 §7.1)。

因此 PC 版是“一个主线程录制 drawlist + 一个渲染线程用立即上下文回放”的双线程模型。该结论与 03 页“PC 版 grcore 不启用子渲染线程”的结论一致,并补全其“游戏层是否另用 deferred context”的未决项。

9. 约束与边界行为#

  1. 只有 update 线程能写命令(operator new 断言);IsBuilding* 系列断言 update 线程,IsExecuting* 系列断言渲染线程。
  2. drawlist 不可嵌套,且只能在 BeginDrawListCreation … EndDrawListCreation 窗口内创建(m_IsDrawListCreationAllowed,assert 构建);dlCmdEndDrawList 不能直接 DLC,必须经 EndCurrentDrawList src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2809-2811。
  3. 命令大小与 ID:12 位大小(×4 字节)、8 位 ID;单条命令断言 < 10000 字节 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2612;矩阵集数据须小于 MAX_DATA_BLOCK_BYTES(骨骼过多会断言)src/dev_ng/game/renderer/DrawLists/drawListCommands_Entities.cpp:449-451。
  4. 队列容量 128:DrawListQ 不阻塞推入方,队满时推入失败且该路径无断言(推断:依赖每帧 drawlist 数远低于 128;主机拆分后的 drawlist 数量需要留意)。
  5. 页耗尽会阻塞主线程(sysIpcSleep(1) 循环),并在第 80/160 次尝试时断言。
  6. 帧号一致性:dlCmdNewDrawList 携带 update 帧号;同步模式下渲染线程断言帧号一致(“Update and render thread are out of sync!”)src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2766-2775。
  7. PC 特殊字段:dlCmdNewDrawList 在 PC 上记录两种帧计数之和并在执行时参与 LOD 偏移计算,源码标注 “HACK - talk to Klaas” 并提到防篡改 src/dev_ng/rage/framework/src/fwdrawlist/drawlist.cpp:2723-2730。按安全规则,本页不展开其检测含义;模块边界见 ../23-debug-security/security-overview.md。

10. 未决问题 / 证据不足#

  • FENCE_COUNT = 4 与 .dtx/头文件注释“三缓冲 / 两帧前”的差异:未建模索引相位,无法给出 GPU 允许落后的精确帧数;PC 不插 fence 的设计理由未找到。
  • dlDrawCommandBuffer::FlipBuffers 注释要求渲染线程不在执行 drawlist,但调用点处渲染线程可能仍在执行本帧前部 drawlist;未找到显式互斥。
  • dlComputeShaderBatch、dlCmdAllocScopePush/Pop(GRCCONTEXT_ALLOC_SCOPES_SUPPORTED)、dlCmdGrcDeviceUpdateBuffer 的内部实现只看到入口。
  • CDrawPedBIGDC/CDrawStreamPedDC/CDrawVehicleVariationDC/CDrawFragDC 等的 Execute 未逐一阅读,只列出数据结构名。