线程模型、任务/依赖调度与渲染线程
本页回答三个问题:GTAV 运行时有哪些线程、各自的优先级与核心亲和如何确定;游戏如何使用 RAGE 的两套并行原语——sysTaskManager(按调度器分组的任务队列)与 sysDependency(带父子依赖的无锁工作图);以及主线程与渲染线程如何以“双信号量 + 安全区”方式逐帧交接。drawlist 的数据结构与双缓冲细节由 ../11-rendering/drawlists-and-render-thread-handoff.md 负责,本页只描述交接协议;sysIpc* 线程/信号量原语本身见 ../02-rage-core/system-and-diagnostics.md。
背景与相关页面#
- 主循环骨架与一帧时序:game-skeleton-and-main-loop.md
- 渲染线程在
CSystem::Init中的创建时机:startup-and-app-fsm.md - 加载期间的阻塞状态与渲染函数切换:session-state-machine.md
- 每个工作线程所属子系统:音频 ../13-audio/game-audio.md、粒子 ../12-vfx/particles-and-ptfx.md、寻路 ../07-ai-tasks/navigation-and-pathserver.md、输入 ../22-input-platform/input-and-control-mapping.md、流式 ../04-rage-framework/streaming-engine.md、回放 ../19-persistence-replay/replay-and-video.md
gameconfig.xml的加载与合并:memory-and-pools.md
相关源码文件#
| 文件 | 作用 |
|---|---|
src/dev_ng/game/system/ThreadPriorities.h | 游戏线程默认优先级常量 |
src/dev_ng/game/system/system.h/.cpp | eThreadId、默认任务调度器参数、CSystem::Init 中的线程配置 |
src/dev_ng/game/system/TaskScheduler.h/.cpp | CTaskScheduler(仅 Xenon 真正异步) |
src/dev_ng/game/game/config.h/.cpp | CThreadSetup、CGameConfig::ThreadPriorityOverrideCb |
src/dev_ng/rage/base/src/system/task.h、task_win32.cpp | sysTaskManager(PC/Durango/Orbis 实现) |
src/dev_ng/rage/base/src/system/dependency.h、dependency_config.h、dependencyscheduler.h/.cpp | sysDependency 与 sysDependencyScheduler |
src/dev_ng/rage/base/src/system/threadtype.h、ipc.cpp | 线程类型位、线程创建与优先级覆盖回调 |
src/dev_ng/rage/framework/src/fwrenderer/renderthread.h/.cpp | fwRenderThreadInterface(gRenderThreadInterface):渲染线程主循环与同步 |
src/dev_ng/game/renderer/RenderThread.h、renderThread.cpp | CGtaRenderThreadGameInterface(安全区操作)与 CGtaOldLoadingScreen |
1. 线程清单#
1.1 核心线程#
| 线程 | 创建者 | 优先级 | 核心/亲和 | 栈 | 证据 |
|---|---|---|---|---|---|
| 主线程(update 线程) | OS(Durango 为 “[RAGE] Main Game Thread”) | 由 gameconfig Update 项决定,缺省保持当前优先级 | 主机缺省 GetCpuAffinityMask(1<<0) | Durango 1 MB | src/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_NORMAL | RENDER_THREAD_CORE = 3 | PC/PS4/XB1 512 KB;Xenon 128 KB | src/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)+2 | 64 位优化构建 144 KB,非优化 300 KB | src/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::AddScheduler | gameconfig systask,缺省 PRIO_ABOVE_NORMAL | 主机掩码 0x2|0x4|0x8|0x20(核 1/2/3/5);PC 为 (1<<n)-1 | 0x10000(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/5 | 64 KB,scratch 32 KB | src/dev_ng/game/physics/WorldProbe/shapetestmgr.cpp:100-107 |
| “[GTA] ControlMgr Update Thread” | CControlMgr::Init | PRIO_HIGHEST | 核 2 | 32 KB | src/dev_ng/game/system/ControlMgr.cpp:295-297 |
| “North Audio Update” | audNorthAudioEngine | PRIO_AUDIO_THREAD = PRIO_HIGHEST | 核 5 | 256 KB | src/dev_ng/game/audio/northaudioengine.cpp:998、src/dev_ng/game/audio/northaudioengine.cpp:481 |
| “RMPTFX Update” | CPtFxManager | PRIO_PTFX_THREAD = PRIO_ABOVE_NORMAL | 核 1 | 56 KB(Orbis 128 KB) | src/dev_ng/game/vfx/particles/PtFxManager.cpp:1350 |
| “PathServerThread” | CPathServerThread | PRIO_PATHSERVER_THREAD = PRIO_NORMAL | m_iProcessorIndex | PATH_SERVER_THREAD_STACKSIZE | src/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 / LOWEST | src/dev_ng/game/control/replay/ReplayInternal.cpp:1014-1018、src/dev_ng/game/control/replay/File/ReplayFileManager.cpp:2500-2508 |
| 存档块写入 “SaveBlockDataThread”、PC 档案保存 “SavePcProfileThread” | BELOW_NORMAL | src/dev_ng/game/SaveLoad/SavegameBuffers/savegame_buffer.cpp:721、src/dev_ng/game/frontend/ProfileSettings.cpp:592 |
| 权益管理器 “CEM Thread”(PC) | BELOW_NORMAL | src/dev_ng/game/system/EntitlementManager.cpp:111 |
| 网络资源哈希、CloudCache 写入、DLC 轮询 | BELOW_NORMAL / NORMAL / LOWEST | src/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 优先级与亲和的三层来源#
- 代码默认值:
ThreadPriorities.h与各sysIpcCreateThread(..., priority, name, cpu, ttyName)调用点。 - 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中更早创建,这表明它们不受覆盖(推断:创建时回调尚未注册)。 - 命令行/注册表:
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并scePthreadAttrSetaffinitysrc/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 |
InitClass | PC 读取处理器拓扑,默认把逻辑核(含超线程)视为核心(-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”,返回 NULL | src/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_Callback | bool (*)(const sysDependency&);返回 false 表示需要重新排队 |
m_NumPending | 尚未完成的“子”(前驱)数量 |
m_Flags | INPUT0..7、OUTPUT0..7、ALLOC0(为参数 0 分配 scratch)src/dev_ng/rage/base/src/system/dependency.h:98-103 |
m_DataSizes[8] | 各数据块大小(ALLOC0 时 [0] 为 scratch 大小) |
m_Priority | kPriorityLow/Med/High/Critical |
m_Chain/m_Id/m_JobFailureCount | PPU/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),上限 64 | src/dev_ng/rage/base/src/system/dependency_config.h:16-25 |
| 唤醒策略 | 插入后按 ceil(numPending / 3)(SYS_DEPENDENCY_TASKWORKERPENDINGRATIO = 3)计算期望 worker 数,kPriorityCritical 额外 +1,唤醒尚未激活的 worker | src/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:
- 填写静态
g_LightsVisibilitySortDependency:kPriorityMed、ALLOC0 | INPUT1..3、参数 1–3 指向输入数组与数据结构,参数 4 指向g_LightsVisibilitySortDependencyRunning,m_DataSizes给出各块大小。 - 置
Running = 1后sysDependencyScheduler::Insert(&dep)。 - 消费者
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:
- Durango 上临时把 hang detect 超时提高到 120 秒(着色器加载可能很慢)。
- 创建同步原语;把
pgDictionaryBase链表所有权置为无效,让渲染线程初始化期间可以“偷走”它。 - 创建线程;失败时断言构建断言、其他构建
Quitf(ERR_GFX_RENDERTHREAD)。 - 主线程每 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 索引并 SignalRenderDone | src/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-496Synchronise:如本帧尚未创建 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、布料缓冲索引轮换;绳索关闭列表;等待布料实例任务 |
| UI | CTextFormat::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不在本源码快照中(按文件名检索无结果),因此各线程实际被覆盖成什么优先级/亲和无法从仓库得出。