CD Code Digest Hub /GTA V · v2

线程模型、任务/依赖调度与渲染线程

57 分钟阅读 13 个章节 1 张图

本页回答三个问题:GTAV 运行时有哪些线程、各自的优先级与核心亲和如何确定;游戏如何使用 RAGE 的两套并行原语——sysTaskManager(按调度器分组的任务队列)与 sysDependency(带父子依赖的无锁工作图);以及主线程与渲染线程如何以“双信号量 + 安全区”方式逐帧交接。drawlist 的数据结构与双缓冲细节由 ../11-rendering/drawlists-and-render-thread-handoff.md 负责,本页只描述交接协议;sysIpc* 线程/信号量原语本身见 ../02-rage-core/system-and-diagnostics.md。

背景与相关页面#

相关源码文件#

文件作用
src/dev_ng/game/system/ThreadPriorities.h游戏线程默认优先级常量
src/dev_ng/game/system/system.h/.cppeThreadId、默认任务调度器参数、CSystem::Init 中的线程配置
src/dev_ng/game/system/TaskScheduler.h/.cppCTaskScheduler(仅 Xenon 真正异步)
src/dev_ng/game/game/config.h/.cppCThreadSetup、CGameConfig::ThreadPriorityOverrideCb
src/dev_ng/rage/base/src/system/task.h、task_win32.cppsysTaskManager(PC/Durango/Orbis 实现)
src/dev_ng/rage/base/src/system/dependency.h、dependency_config.h、dependencyscheduler.h/.cppsysDependency 与 sysDependencyScheduler
src/dev_ng/rage/base/src/system/threadtype.h、ipc.cpp线程类型位、线程创建与优先级覆盖回调
src/dev_ng/rage/framework/src/fwrenderer/renderthread.h/.cppfwRenderThreadInterface(gRenderThreadInterface):渲染线程主循环与同步
src/dev_ng/game/renderer/RenderThread.h、renderThread.cppCGtaRenderThreadGameInterface(安全区操作)与 CGtaOldLoadingScreen

1. 线程清单#

1.1 核心线程#

线程创建者优先级核心/亲和栈证据
主线程(update 线程)OS(Durango 为 “[RAGE] Main Game Thread”)由 gameconfig Update 项决定,缺省保持当前优先级主机缺省 GetCpuAffinityMask(1<<0)Durango 1 MBsrc/dev_ng/game/system/system.cpp:744-752、src/dev_ng/rage/base/src/system/applicationview_durango.winrt.cpp:256-257
渲染线程 “[RAGE] RenderThread”fwRenderThreadInterface::Init(在 CSystem::Init 中)PRIO_RENDER_THREAD = PRIO_BELOW_NORMALRENDER_THREAD_CORE = 3PC/PS4/XB1 512 KB;Xenon 128 KBsrc/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:308-309、src/dev_ng/rage/framework/src/fwrenderer/renderthread.h:24-34、src/dev_ng/game/system/ThreadPriorities.h:8
子渲染线程 “Subrender:N” ×4(仅 PS4/XB1,MULTIPLE_RENDER_THREADS)dlDrawListMgr::ExecuteAndRemoveAll 首次调用时PRIO_IDLE不绑定(-1)–src/dev_ng/rage/base/src/grcore/effect_mrt_config.h:2-6、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:1455-1478
依赖工作线程 “[RAGE] Dependency N”sysDependencyScheduler::InitClass(CommonMain_Prologue)PRIO_HIGHEST主机 {1,2,3,5};PC (i%5)+264 位优化构建 144 KB,非优化 300 KBsrc/dev_ng/rage/base/src/system/dependencyscheduler.cpp:143-170、src/dev_ng/rage/base/src/system/dependency_config.h:16-44
任务调度器 “gtaSched:N”CSystem::Init → sysTaskManager::AddSchedulergameconfig systask,缺省 PRIO_ABOVE_NORMAL主机掩码 0x2|0x4|0x8|0x20(核 1/2/3/5);PC 为 (1<<n)-10x10000(dev 0x18000),64 位翻倍src/dev_ng/game/system/system.cpp:716-738、src/dev_ng/game/system/system.h:21-22
形状测试调度器 “ShapeTestAsyncSched:N”WorldProbe::CShapeTestManager 初始化PRIO_SHAPETEST_ASYNC_THREAD = PRIO_ABOVE_NORMAL核 1/2/3/564 KB,scratch 32 KBsrc/dev_ng/game/physics/WorldProbe/shapetestmgr.cpp:100-107
“[GTA] ControlMgr Update Thread”CControlMgr::InitPRIO_HIGHEST核 232 KBsrc/dev_ng/game/system/ControlMgr.cpp:295-297
“North Audio Update”audNorthAudioEnginePRIO_AUDIO_THREAD = PRIO_HIGHEST核 5256 KBsrc/dev_ng/game/audio/northaudioengine.cpp:998、src/dev_ng/game/audio/northaudioengine.cpp:481
“RMPTFX Update”CPtFxManagerPRIO_PTFX_THREAD = PRIO_ABOVE_NORMAL核 156 KB(Orbis 128 KB)src/dev_ng/game/vfx/particles/PtFxManager.cpp:1350
“PathServerThread”CPathServerThreadPRIO_PATHSERVER_THREAD = PRIO_NORMALm_iProcessorIndexPATH_SERVER_THREAD_STACKSIZEsrc/dev_ng/game/pathserver/PathServerThread.cpp:347-362
流式读取线程pgStreamer::InitClass(cpuId, PRIO_STREAMER_THREAD)PRIO_NORMAL(gameconfig streamer 可覆盖)Xenon/Durango cpuId 1,其余 0–src/dev_ng/game/system/system.cpp:511-516、src/dev_ng/game/streaming/streamingdebug.cpp:6744
hang detect 线程CommonMain_Prologue(无调试器时)–––src/dev_ng/rage/base/src/system/main.cpp:1047-1056

ThreadPriorities.h 还定义了 PRIO_MOTION_TREE_THREAD(BELOW_NORMAL)、PRIO_NETWORK_ASSET_VERIFIER_THREAD(BELOW_NORMAL)、PRIO_GETSAVENAMES_THREAD(NORMAL)、PRIO_SHAPETEST_DISPATCHER_THREAD(PRIO_LOWEST-1)、PRIO_SYSTEM_INFO_THREAD、PRIO_NETWORK_HASHER_UTIL_THREAD,并注明“main thread = PRIO_NORMAL” src/dev_ng/game/system/ThreadPriorities.h:6-18。

1.2 其他按需线程(摘录)#

线程优先级证据
回放录制 ×4(Default/Ped/Vehicle/Game&Object)、Quantize、文件管理器、目录监视ABOVE_NORMAL / BELOW_NORMAL / LOWESTsrc/dev_ng/game/control/replay/ReplayInternal.cpp:1014-1018、src/dev_ng/game/control/replay/File/ReplayFileManager.cpp:2500-2508
存档块写入 “SaveBlockDataThread”、PC 档案保存 “SavePcProfileThread”BELOW_NORMALsrc/dev_ng/game/SaveLoad/SavegameBuffers/savegame_buffer.cpp:721、src/dev_ng/game/frontend/ProfileSettings.cpp:592
权益管理器 “CEM Thread”(PC)BELOW_NORMALsrc/dev_ng/game/system/EntitlementManager.cpp:111
网络资源哈希、CloudCache 写入、DLC 轮询BELOW_NORMAL / NORMAL / LOWESTsrc/dev_ng/game/network/General/NetworkHasherUtil.cpp:380、src/dev_ng/game/network/Cloud/CloudCache.cpp:340、src/dev_ng/game/scene/ExtraContent.cpp:1496-1501
用户音乐扫描(PC)、调试条 “System Info Thread”、Bugstar 断言上报–src/dev_ng/game/audio/userradiotrackmanager.cpp:965、src/dev_ng/game/debug/Bar.cpp:349、src/dev_ng/game/debug/BugstarIntegration.cpp:538

另有 Durango 的主应用线程(只泵 CoreWindow 事件,见 startup-and-app-fsm.md)与 PC 的焦点查询线程(GRCDEVICE.InitFocusQueryThread(),src/dev_ng/game/system/system.cpp:693)。

2. 线程身份与优先级配置#

2.1 线程类型位#

sysThreadType 用位标识线程角色:THREAD_TYPE_UPDATE(BIT0)、THREAD_TYPE_RENDER(BIT1)、THREAD_TYPE_PROCESS_MAIN(BIT2)、THREAD_TYPE_DEPENDENCY(BIT3),用户类型从 BIT4 起 src/dev_ng/rage/base/src/system/threadtype.h:17-22。游戏在 eThreadId 中追加 SYS_THREAD_AUDIO/PATHSERVER/PARTICLES/POSTSCAN,“用位标志是因为一个线程可能同时是主线程与渲染线程” src/dev_ng/game/system/system.h:24-34。

这些位用于运行时断言:fwRenderThreadInterface::GetUpdateBuffer() 断言调用线程拥有 m_UpdateBufferThreadAccess 中的某个位,GetRenderBuffer() 断言是渲染线程 src/dev_ng/rage/framework/src/fwrenderer/renderthread.h:139-141;游戏在渲染线程初始化时把可访问 update 缓冲的线程放宽为 UPDATE | AUDIO | PARTICLES src/dev_ng/game/renderer/renderThread.cpp:581-591。

2.2 优先级与亲和的三层来源#

  1. 代码默认值:ThreadPriorities.h 与各 sysIpcCreateThread(..., priority, name, cpu, ttyName) 调用点。
  2. gameconfig 覆盖:CGameConfig 构造时注册 sysIpcSetThreadPriorityOverrideCb(ThreadPriorityOverrideCb) src/dev_ng/game/game/config.cpp:472-480;此后每次 sysIpcCreateThreadWithCoreMask 若提供了 threadTtyName,就以其 atStringHash 查 CGameConfig::m_Threads,用其中的 CThreadSetup{m_Priority, m_CpuAffinity} 覆盖优先级与亲和 src/dev_ng/rage/base/src/system/ipc.cpp:1067-1072、src/dev_ng/game/game/config.cpp:571-598、src/dev_ng/game/game/config.h:470-486。例如渲染线程的 tty 名为 "Render"、音频线程为 "NorthAudioUpdate"、粒子为 "RmptfxUpdateThread"。由于 gameconfig 在 CSystem::Init 中加载,而依赖工作线程与 hang detect 线程在 CommonMain_Prologue 中更早创建,这表明它们不受覆盖(推断:创建时回调尚未注册)。
  3. 命令行/注册表:THREAD_REGISTRY 下 sysThreadRegistry::AdjustPriosBasedOnParams() 允许命令行再次调整,并把主线程登记为 “Main Thread” src/dev_ng/game/system/system.cpp:773-777。

特殊规则:

  • PC 低配机:CSettingsManager::IsLowQualitySystem()(注释:AMD 最低配显卡 + 4 核,单线程驱动导致严重渲染线程瓶颈)时,交换主线程与渲染线程的优先级 src/dev_ng/game/system/system.cpp:754-771。
  • 平台亲和实现:PC 上 sysIpcCreateThread 忽略 cpu 参数(掩码固定为 -1),只有 -setSystemProcessorAffinity 会给每个线程设置同一全局掩码;Durango 默认掩码排除核 0(注释解释了微软调度器在主线程睡眠后不及时归还核心的问题),并在未给 -useSystemThreadManagement 时设置组亲和;Orbis 使用 SCHED_RR 并 scePthreadAttrSetaffinity src/dev_ng/rage/base/src/system/ipc.cpp:1037-1064、src/dev_ng/rage/base/src/system/ipc.cpp:1104-1143、src/dev_ng/rage/base/src/system/ipc.cpp:1151-1166。Win32 上还对每个线程 SetThreadPriorityBoost(h, TRUE)(禁用动态提升)src/dev_ng/rage/base/src/system/ipc.cpp:1100-1103。
  • PC final 构建在调用点把线程名替换为空串 src/dev_ng/rage/base/src/system/ipc.cpp:1183-1188。
  • 新线程继承创建者的当前分配器,并断言创建时 Current 与 Master 相同(防止在调试堆激活时创建线程)src/dev_ng/rage/base/src/system/ipc.cpp:665-673。

3. sysTaskManager 与 CTaskScheduler#

3.1 sysTaskManager(PC/Durango/Orbis 实现)#

task_win32.cpp 在 __WIN32 || __PSP2 || RSG_ORBIS 下编译,即同时服务 PC、Xbox One 与 PS4 src/dev_ng/rage/base/src/system/task_win32.cpp:10。

要素实现证据
容量HACK_GTA4 下 MaxTasks = 512、MaxSchedulers = 32(否则 256/8);任务句柄是静态数组 + u16 空闲栈(O(1) 分配)src/dev_ng/rage/base/src/system/task_win32.cpp:60-81
调度器sysTaskScheduler = 每个掩码位一个工作线程(名为 name:core)+ 一个容量 MaxTasks 的 sysMessageQueue;工作线程的 scratch 与栈在 64 位上翻倍,Orbis 非优化构建栈再翻倍src/dev_ng/rage/base/src/system/task_win32.cpp:104-166
工作循环阻塞 Pop 任务 → 拷贝头、绑定本线程 scratch → 调用 Header.Func(Parameters) → sysIpcSetEvent(Completed);Bank 下可被禁用挂起src/dev_ng/rage/base/src/system/task_win32.cpp:193-248
InitClassPC 读取处理器拓扑,默认把逻辑核(含超线程)视为核心(-disableHyperthreading 可关),-numCores/-maxthreads 限制;为每个句柄创建完成事件src/dev_ng/rage/base/src/system/task_win32.cpp:421-474
Create/Prepare/Dispatch在临界区内分配句柄并压入指定调度器队列;若尚无调度器,自动创建 "defSched"(PRIO_LOWEST,全部核心)src/dev_ng/rage/base/src/system/task_win32.cpp:538-600
句柄耗尽断言构建打印所有作业并断言“Out of jobs”,返回 NULLsrc/dev_ng/rage/base/src/system/task_win32.cpp:506-521

游戏创建的调度器:"gtaSched"(索引 0,即默认调度器)由 CSystem::Init 添加 src/dev_ng/game/system/system.cpp:738,PC 上 worker 数默认取 SYS_DEPENDENCY_TASKACTIVENUMWORKERS = Max(GetNumThreads() - 2, 1),可用 -numTaskSchedulers 覆盖 src/dev_ng/game/system/system.cpp:727-736、src/dev_ng/rage/base/src/system/dependency_config.h:17-20;形状测试另建专用调度器(见 1.1)。PS3 的 SPURS 调度器枚举(SCHEDULER_DEFAULT_SHORT 等)仍保留在 task.h 中但属于遗留路径 src/dev_ng/rage/base/src/system/task.h:79-110。

3.2 CTaskScheduler:只在 Xenon 上异步#

CTaskScheduler 把最多 MAX_USER_DATA_IN_TASK = 3 个参数的小任务批量打包进一个 sysTaskParameters,最多保留 NUM_TASKS_IN_MGR = 32 个在途句柄 src/dev_ng/game/system/TaskScheduler.h:18-58。但 USE_TASK_SCHEDULER 定义为 __XENON;在其他平台上 AddTask 直接同步调用函数,Flush/WaitOnResults/StartBatchingTasks 为空操作 src/dev_ng/game/system/TaskScheduler.cpp:17-115。主循环每帧在渲染交接前 StartBatchingTasks(5),并在安全区 WaitOnResults() src/dev_ng/game/Core/app.cpp:1362、src/dev_ng/game/renderer/renderThread.cpp:387-390。

4. sysDependency:依赖图工作调度#

4.1 数据结构#

sysDependency(16 字节对齐)描述一个原子工作包 src/dev_ng/rage/base/src/system/dependency.h:24-94:

字段含义
m_Params[10]参数联合体(float/int/指针/两个 u16)
m_Parents[2]kParentStrong 与 kParentWeak 两个“父”(后继)指针
m_Callbackbool (*)(const sysDependency&);返回 false 表示需要重新排队
m_NumPending尚未完成的“子”(前驱)数量
m_FlagsINPUT0..7、OUTPUT0..7、ALLOC0(为参数 0 分配 scratch)src/dev_ng/rage/base/src/system/dependency.h:98-103
m_DataSizes[8]各数据块大小(ALLOC0 时 [0] 为 scratch 大小)
m_PrioritykPriorityLow/Med/High/Critical
m_Chain/m_Id/m_JobFailureCountPPU/SPU 链(仅 PS3 有 SPU 链)、性能统计标签(5 位)、失败计数

命名容易误解:a.AddChild(b) 的含义是“b 完成后才能运行 a”——它把 b.m_Parents[strong] = &a 并递增 a.m_NumPending src/dev_ng/rage/base/src/system/dependency.h:134-148。即“父”是后继/汇合点,只有 m_NumPending == 0 的叶子才能 Insert src/dev_ng/rage/base/src/system/dependencyscheduler.cpp:201-205。

4.2 调度器#

机制实现证据
就绪队列一个 Chain:每个优先级一个 sysLockFreeRing<sysDependency, 2048>;Pop 从最高优先级开始取src/dev_ng/rage/base/src/system/dependencyscheduler.cpp:35-92
工作线程数主机 4;PC Max(GetNumThreads()-2, 1),上限 64src/dev_ng/rage/base/src/system/dependency_config.h:16-25
唤醒策略插入后按 ceil(numPending / 3)(SYS_DEPENDENCY_TASKWORKERPENDINGRATIO = 3)计算期望 worker 数,kPriorityCritical 额外 +1,唤醒尚未激活的 workersrc/dev_ng/rage/base/src/system/dependencyscheduler.cpp:239-253、src/dev_ng/rage/base/src/system/dependency_config.h:13-14
worker 循环取不到任务时先把自己标为非激活再尝试一次 Pop,仍为空则等待事件;执行回调前拷贝父指针;ALLOC0 时把 96 KB(64 位)栈上 scratch 交给参数 0,Bank 构建在超大请求时改用堆,其他构建超限则致命断言src/dev_ng/rage/base/src/system/dependencyscheduler.cpp:289-354、src/dev_ng/rage/base/src/system/dependency_config.h:39-44
完成传播回调返回 true:对每个父原子递减 m_NumPending,归零的父中第一个优先级不低于当前者被本线程直接接手(避免往返队列),其余重新插入;返回 false:以降一级优先级重新入队src/dev_ng/rage/base/src/system/dependencyscheduler.cpp:383-417
调试-singleThreadedTaskManager 使 Insert 在调用线程上先把队列跑空src/dev_ng/rage/base/src/system/dependencyscheduler.cpp:228-237

4.3 游戏侧的使用模式#

游戏代码不使用通用的“等待句柄”,而是约定一个由回调清零的 u32 Running 标志,由消费者自旋等待。以灯光可见性排序为例 src/dev_ng/game/renderer/Lights/lights.cpp:2240-2276:

  1. 填写静态 g_LightsVisibilitySortDependency:kPriorityMed、ALLOC0 | INPUT1..3、参数 1–3 指向输入数组与数据结构,参数 4 指向 g_LightsVisibilitySortDependencyRunning,m_DataSizes 给出各块大小。
  2. 置 Running = 1 后 sysDependencyScheduler::Insert(&dep)。
  3. 消费者 Lights::WaitForProcessVisibilityDependency() 以 sysIpcYield(PRIO_NORMAL) 自旋直到标志清零——它被安排在渲染线程交接的安全区中调用(见第 7 节)。

直接调用 sysDependencyScheduler::Insert 的游戏文件包括:灯光(lights.cpp、LODLights.cpp、AsyncLightOcclusionMgr.cpp)、PlantsMgr.cpp、PostScan.cpp、SceneStreamerList.cpp、TimeCycleAsyncQueryMgr.cpp、VehicleLightAsyncMgr.cpp、vehiclepopulation.cpp、wheel.cpp、DistantLights*.cpp、drawListCommands_Entities.cpp 以及安全模块中的一个文件;框架层的可见性扫描(fwscene/scan)、搜索、渲染列表、drawlist 管理器,以及 suite 层的软件光栅化遮挡与动作树调度器也构建在它之上(来自全文检索,未逐一阅读)。

5. 渲染线程:创建、初始化与主循环#

5.1 同步原语#

ThreadParams 包含四个信号量与一个互斥体:m_eKill、m_eRunning、m_eUpdateDone、m_eRenderDone、m_customRenderThreadMutex(自定义渲染函数执行期间持有)src/dev_ng/rage/framework/src/fwrenderer/renderthread.h:43-75,信号量均以初值 0 创建 src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:110-119。fwRenderThreadInterface 还维护 m_updateBuffer/m_renderBuffer 两个 0/1 索引(FlipUpdateBuffer 在 drawlist 创建结束时翻转,FlipRenderBuffer 在渲染线程每帧末翻转)src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:498-506、src/dev_ng/rage/framework/src/fwdrawlist/drawlistmgr.cpp:963-979。

5.2 创建与设备移交#

fwRenderThreadInterface::Init(priority, initCallback) src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:288-343:

  1. Durango 上临时把 hang detect 超时提高到 120 秒(着色器加载可能很慢)。
  2. 创建同步原语;把 pgDictionaryBase 链表所有权置为无效,让渲染线程初始化期间可以“偷走”它。
  3. 创建线程;失败时断言构建断言、其他构建 Quitf(ERR_GFX_RENDERTHREAD)。
  4. 主线程每 500 ms 轮询 m_eRunning(同时泵 g_SysService)直到渲染线程完成初始化;随后取回字典链表所有权、m_bActive = true。

渲染线程入口 RenderThreadEntryPoint 设置随机种子并标记 THREAD_TYPE_RENDER,进入 MainLoop() src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:1004-1020。MainLoop 先执行初始化回调(游戏传入的 CSystem::RenderThreadInit:BeginGfx、着色器预初始化、CStreaming::InitStreamingEngine、创建 gDrawListMgr、CGtaRenderThreadGameInterface::InitClass 等,见 startup-and-app-fsm.md),初始化计时条,然后 SignalRunning(),再调用游戏接口的 RenderThreadInit()(CSpuPmMgr::Init、放宽 update 缓冲访问)src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:679-703。之后循环直到 m_eKill:在临界区内把“待定渲染函数”切换为当前渲染函数;RSG_LOST_FOCUS_SUPPORT 下处理设备管理与失焦暂停;然后 DoRenderFunction() src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:711-785。

CGtaRenderThreadGameInterface::InitClass 把游戏接口装入 gRenderThreadInterface,PC 上还把 GRCDEVICE.RenderFunctionCallback 设为直接执行一次渲染函数(供设备层在需要时回调)src/dev_ng/game/renderer/renderThread.cpp:187-200。

5.3 关闭#

Shutdown():SignalKill → 一次“迷你刷新”(SignalUpdateDone + TerminateDrawLists)→ PC 解锁设备重置并注销会话通知 → 等待线程退出 → 销毁原语 src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:345-365。由 CGame::Shutdown 在 SHUTDOWN_CORE 之前调用(设备回到主线程)src/dev_ng/game/Core/game.cpp:1215。

6. 自定义渲染函数与加载画面#

渲染线程每次迭代执行“当前渲染函数”:NULL 表示默认函数(执行 drawlist),非空表示一个自定义 Functor0(加载画面、安装进度、语言选择、落地页等)。

函数行为证据
SetRenderFunction(fn)断言当前不在构建 drawlist;在临界区内更新待定函数,变化时回调游戏的 RenderFunctionChanged(old, new)src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:984-1002
DoRenderFunction(自定义分支)持有 m_customRenderThreadMutex 调用自定义函数(PC 仅在设备就绪时),主机上随后 sysIpcSleep(2) 让出核心(B* 2107124/2109381:防止饿死低优先级线程);轮询 PollUpdateDone():若主线程恰好完成了一次同步,则丢弃所有 drawlist(RemoveAll)、翻转渲染缓冲与 fence 索引并 SignalRenderDonesrc/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:800-854
默认分支见第 7 节src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:866-982

游戏侧的选择逻辑在 CGtaOldLoadingScreen::SetLoadingScreenIfRequired():加载画面活跃 → CLoadingScreens::RenderLoadingScreens;否则若被强制、或(暂停菜单/片尾字幕/警告屏/脚本均未阻止且画面已淡出)→ “Loading...” 渲染函数;否则默认函数 src/dev_ng/game/renderer/renderThread.cpp:701-727。它每帧由 CViewportManager::Process() 调用(即 FULL/SIMPLE_MAIN_UPDATE 中的 “Viewport Manager” 条目)src/dev_ng/game/camera/viewports/ViewportManager.cpp:387-398;CLoadingScreens::Init 则直接设置渲染函数并 Flush(true) 确保渲染线程立即采用 src/dev_ng/game/frontend/loadingscreens.cpp:856-859。RenderFunctionChanged 在进入/离开 “Loading...” 函数时禁用/恢复玩家控制,离开时在允许的情况下结束用户暂停 src/dev_ng/game/renderer/renderThread.cpp:650-683。

这解释了加载期间的行为:主线程阻塞在会话状态机或 gameSkeleton::Init 中、不再调用 Synchronise(),而渲染线程独立循环执行加载画面函数;主线程侧通过 keep-alive 回调与 FlushAndDeviceCheck() 维持其余状态。

7. 帧同步与安全区#

7.1 协议#

图中证据:

  • 渲染线程默认分支:DefaultPreRenderFunction → gDrawListMgr->ExecuteAndRemoveAll() → DefaultPostRenderFunction → FlipRenderBuffer → FlipRenderFenceIdx → WaitTillUpdateDone() → 帧号++ → SignalRenderDone() → sysIpcSleep(0):src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:919-971
  • GPU_IdleSection:断言 update 线程、翻转 update fence 索引、主机上 CpuWaitOnFence(上一帧 end fence)、RemoveAllRefs()、Durango 派发延迟系统事件:src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:449-496
  • Synchronise:如本帧尚未创建 drawlist 则创建空帧;SignalUpdateDone;检查信号量完整性;WaitTillRenderDone(hang detect 崩溃区内);SetToPreDrawListState;断言渲染线程不在执行 drawlist;进入安全区:src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:377-447
  • 调用顺序(GPU_IdleSection → Synchronise → CGame::Render):src/dev_ng/game/Core/app.cpp:1361-1370

关键点:

  • 安全区为何安全:Synchronise 中的注释写明“render thread is guaranteed to be blocked here”src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:419-421。从协议看,渲染线程此时已离开上一帧的 drawlist 执行,下一步要么在 ExecuteAndRemoveAll 中等待本帧第一个 drawlist(主线程尚未开始构建),要么在执行默认的前置函数。drawlist 管理器内部的等待机制见 ../11-rendering/drawlists-and-render-thread-handoff.md。
  • 流水线深度:主线程更新帧 N 的同时,渲染线程执行帧 N-1 的 drawlist;主机上 GPU 还可能在处理更早的帧,由 GPU_IdleSection 的 fence 等待限制(PC 不在此等待)。因此 CPU 侧是“update 与 render 重叠一帧”的双缓冲结构。
  • D3D11 上下文:PC 在安全区持有 GRCDEVICE.LockContext(),若待定渲染函数不是默认函数,还在此执行挂起的纹理/缓冲更新(因为没有 drawlist 可放);并在 UnlockDeviceResetAvailability/LockDeviceResetAvailability 之间控制设备重置只能发生在同步点 src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:405-444。
  • 计时:m_TimeWaitedForGpu 与渲染线程侧 m_TimeWaitedForUpdate 被安全区写入 perfClearingHouse(WAIT_FOR_UPDATE、WAIT_FOR_GPU)src/dev_ng/game/renderer/renderThread.cpp:547-553。

7.2 安全区里做什么:CGtaRenderThreadGameInterface::PerformSafeModeOperations#

在渲染线程不访问共享数据的窗口里,游戏完成所有“双缓冲翻转”和需要独占访问的更新 src/dev_ng/game/renderer/renderThread.cpp:265-579。按顺序:

类别操作
校验若有人忘记弹出当前纹理字典(fwTxd::GetCurrent() 非空)则 Quitf
回放/录像VideoRecording::Update、CReplayMgr::ProcessRecord
依赖结果回收PC 河流流向图、Lights::WaitForProcessVisibilityDependency()、高质量截图
调试Bank 着色器编辑、纹理查看器、灯光编辑、帧 dump
同步/翻转级联阴影、抛物面阴影、PostFX 自适应、灯光、LOD 灯、过场 Synchronise();远处灯光顶点缓冲翻转;树木微动;绳索 SafeUpdate;布料清理
特效CVisualEffects::UpdateSafe、照片编辑器(非开局自动读档时)
任务CTaskScheduler::WaitOnResults()
渲染目标交换 PtFx GPU 碰撞高度图双缓冲、清理命名渲染目标
粒子g_ptFxManager.BeginFrame()(粒子 drawlist 翻转)、排序实例加入渲染列表;随后 StartLocalEffectsUpdate() 唤醒粒子更新线程
渲染列表CGtaRenderListGroup::ProcessDeferredAdditions()
碎片整理USE_DEFRAGMENTATION 时在 CTexLod::SetSwapStateAll 包裹下执行 strStreamingEngine::GetDefragmentation()->Update()(注释:尽量靠后以等待 drawable SPU 工作完成);由于 ENABLE_KNOWN_REFERENCES 为 0,本快照中这段代码不参与编译(见 streaming-engine.md §7.2)
动画/水/植被fwAnimDirector::UpdateClass、Water::FlipBuffers、CPlantMgrWrapper::UpdateSafeMode
GPU 查询遮挡查询、水洼查询、镜头光晕查询结果
玻璃/布料/几何玻璃延迟删除、bgDrawable::EndFrame;未暂停时 grmGeometryQB::SwapBuffers、布料缓冲索引轮换;绳索关闭列表;等待布料实例任务
UICTextFormat::EndFrame、CScaleformMgr::UpdateAtEndOfFrame(true)、CNewHud/CMiniMap/CMultiplayerGamerTagHud::UpdateAtEndOfFrame
其他强制纹理字典清理、MeshBlendManager::UpdateAtEndOfFrame、性能计数、PC 设置管理器安全更新与多 GPU 索引、TamperActions::PerformSafeModeOperations(安全模块,见 ../23-debug-security/security-overview.md)

开销预算为 1 ms(PF_AUTO_PUSH_TIMEBAR_BUDGETED("Safe Execution", 1.0f))。

8. Flush 与 FlushAndDeviceCheck#

Flush(forceFullFlush) 用于在不渲染新内容的情况下把渲染管线排空(换关、语言切换、加载画面切换等)src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:509-643:

  • 只允许在 update 线程调用,且不能在构建 drawlist 时调用;gDrawListMgr 尚未创建(启动早期出错)时直接返回。
  • 若上次已刷新且未强制:只对新增引用做 RemoveAllRefs(FLUSH_WITHOUT_RENDERING) 后返回。
  • 否则执行四轮“GPU_IdleSection → SignalUpdateDone → 轮询 PollIsRenderDone(1 ms 睡眠并泵系统服务)→ SetToPreDrawListState → 创建空 drawlist 帧”;随后调用游戏的 Flush()(粒子 BeginFrame、布料清理、CTexLod::FlushMemRemaps)src/dev_ng/game/renderer/renderThread.cpp:685-697,并把设备同步计数器加 2,防止 IsSafeToDelete 误判。
  • PC 的 LOG_RENDER_FLUSH(非 final)记录主/渲染线程环形日志,hang detect 触发时按时间合并输出 src/dev_ng/rage/framework/src/fwrenderer/renderthread.h:13、src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:145-250、src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:339-341。

FlushAndDeviceCheck()(PC)在 Flush() 之后锁上下文执行挂起的缓冲更新,供会话状态机的阻塞循环在没有正常帧时保持 D3D11 设备健康 src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:645-666。

9. PC 特有:设备丢失与失焦#

  • 默认渲染函数在设备未就绪、或“失焦且允许失焦暂停”(非 final 需 -BlockOnLostFocus)时不执行 drawlist:等待 update 完成后直接丢弃全部 drawlist、刷新 Scaleform 缓冲并翻转;设备恢复后最多强制丢弃 5 帧残留 drawlist(防止旧分辨率的 drawlist 被执行)src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:861-916。
  • RSG_LOST_FOCUS_SUPPORT 下渲染线程在 GRCDEVICE.BlockUpdateThread() 时以 50 ms 间隔空转(并开始 fwTimer 失焦暂停),恢复后结束暂停;有 5 次“穿透”计数缓冲 src/dev_ng/rage/framework/src/fwrenderer/renderthread.cpp:735-782。
  • 设备丢失/重置回调由 CGame::InitCallbacks 注册(见 startup-and-app-fsm.md)。

10. 约束与边界行为#

  • 主线程独占的操作:Flush、GPU_IdleSection、CGame 生命周期函数都断言 update 线程;SetRenderFunction 断言不在构建 drawlist。
  • 安全区之外不得修改渲染线程读取的双缓冲数据:所有翻转集中在 PerformSafeModeOperations 中;新增需要与渲染线程交换的数据时,应在此处翻转或通过 drawlist 命令传递。
  • 线程创建时机影响配置:在 gameconfig 加载之前创建的线程无法被覆盖优先级/亲和(推断,见 2.2)。
  • 依赖调度器的 scratch 上限:非 Bank 构建中 ALLOC0 请求超过 SYS_DEPENDENCY_WORKERSCRATCH(64 位 96 KB)会致命断言。
  • CTaskScheduler 平台语义不同:在非 Xenon 平台上它是同步调用,不能假设任务与调用者并行。

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

  • pgStreamer 内部创建多少读取线程、各自优先级,属于流式页面范围,本页未追踪。
  • 动作树调度器(PRIO_MOTION_TREE_THREAD)在游戏中的实际创建点未定位;该常量在 game/ 中只在 ThreadPriorities.h 出现。
  • sysThreadRegistry::AdjustPriosBasedOnParams 支持的参数格式未展开。
  • gameconfig.xml 不在本源码快照中(按文件名检索无结果),因此各线程实际被覆盖成什么优先级/亲和无法从仓库得出。