游戏引擎架构深度解析(二):渲染系统架构,这个题目我拖了挺久才动笔。原因也很简单,比起上一期讲引擎整体模块划分和ECS那套东西,渲染系统是真正能把一个引擎的底裤都扯出来的部分。很多刚接触引擎开发的同学都有个错觉,觉得渲染嘛,无非就是设置渲染状态、提交DrawCall、最后把交换链往屏幕上一怼完事。等你真去落地一个跨平台项目,会发现事情远不是这么简单:CPU这边场景遍历和绘制命令生成的速度,GPU那边异步执行的节奏,两者之间还夹着资源上传、状态同步、内存生命周期这一堆"隐性成本",任何一个环节接不上,帧率立刻给你脸色看。
这篇文章我会从架构设计的角度,把渲染系统拆开讲清楚。重点不是教你怎么调API,而是把"渲染系统到底在管理什么复杂性""线程模型怎么搭""CPU和GPU怎么握手""资源生命周期怎么管"这几个核心问题梳理明白。适合正在写或者准备写自研引擎、想做引擎渲染层重构的开发者,也适合那些"只在业务层调引擎"但想搞懂底层原理的同学。
1. 先理清概念:渲染系统架构这个命题的边界
渲染系统架构不是"画三角形"的代码组织方式,它是一整套把场景数据、资源数据、GPU执行能力耦合起来的管理机制。很多半路出家的引擎项目,前期跑得飞快,越到后面越动不了,问题基本都出在架构边界没画清楚:渲染逻辑、资源管理、线程同步全揉在一起,改一处崩三处。
1.1 渲染系统的本质:管理"异步度"和"状态性"
想理解渲染系统的架构设计,得先抓住两个核心矛盾。
第一个矛盾是异步度。CPU和GPU是两台独立的机器,CPU负责生成指令流,GPU负责执行指令流。指令从生成到执行,中间隔着提交队列、命令处理器、显存带宽,这中间的时间差少则一帧,多则两三帧。架构上如果不去管理这个时间差,就会频繁出现"CPU改了个值,GPU还在用旧值"这种数据竞争问题。不是线程级竞争,而是跨设备的竞争,调试起来极其恶心。
第二个矛盾是状态性。渲染API几乎都是强状态机:当前绑定了什么Shader、什么RenderTarget、什么顶点缓冲,驱动内部都有一大堆状态在相互约束。这种状态性对架构的要求极其苛刻——你要是让业务逻辑层随手就能修改某个全局状态,那整个渲染管线的提交顺序和缓存策略瞬间就乱套了。
实际上,一个好的渲染系统架构,真正的任务就是两条:把异步度管成"可预测的延迟",把状态性管成"可追踪的变更"。这两条做到位了,后面接什么功能都顺手。
1.2 渲染系统的复杂性分布:时间到底花在哪了
我见过不少团队拿着性能分析器看渲染耗时,只盯着DrawCall数和GPU时间,却忽略了一个更关键的问题——CPU侧的耗时分布。渲染一帧的开销,从来都不只是在GPU上。我用一张大致的时间分布表来说明,实际占比因场景而异:
| 环节 | 主要内容 | 典型CPU耗时占比 |
|---|---|---|
| 场景遍历与裁剪 | 剔除、LOD选择、渲染对象收集 | 20% ~ 35% |
| 渲染数据准备 | 矩阵计算、光照参数收集、材质参数绑定 | 15% ~ 25% |
| 绘制命令提交 | DrawCall生成、状态切换、命令列表写入 | 25% ~ 40% |
| 资源同步与上传 | 纹理/网格上传、Buffer更新、Barrier处理 | 10% ~ 20% |
| 同步与等待 | 信号量、Fence等待、GPU回读 | 5% ~ 15% |
注意那个"同步与等待"——架构不好的渲染系统,这块比例会膨胀到可怕的程度。这就是为什么我一直强调,渲染架构的第一优先级不是把单核的提交速度压到极致,而是把整个管线的节奏设计好,让CPU提交、GPU执行、资源上传三路并行,把等待时间压缩到接近零。
1.3 架构设计的第一原则:让状态变化可追踪
我在做渲染层重构的时候,定的第一条纪律就是:所有渲染状态的变更,都必须经过渲染线程的统一入口,禁止业务层直接触碰底层API对象。听起来像废话,但实际项目中违反这条纪律的代码多得吓人。
比如美术系统为了做特效,直接拿驱动层的DeviceContext去改深度状态,临时调一次RenderTarget,调用完还不恢复——这种代码一次两次没出问题,等某个特性上线突然大面积渲染异常,查半天发现是一周前某段特效代码留下的状态残留。这种问题跟你的逻辑写得好不好没关系,纯属架构边界失守。
所以架构上要做的,不是禁止大家改状态,而是把所有状态的变更收拢到渲染队列里,像流水线一样顺序执行。背后的秩序感,比任何奇技淫巧都重要。
2. 渲染线程模型:同步这件事写在架构里
渲染系统架构里最伤筋动骨的部分,就是线程模型。它对最终性能的影响面最大,改动成本也最高,几乎决定了你能不能在多核处理器上吃满性能红利。说实话,这一块我踩的坑比写代码的时间还多。
2.1 为什么渲染不能跟在逻辑线程后面直接画
很多人最早写游戏渲染代码的逻辑是:主循环里先更新逻辑,再调渲染接口画一帧,完事。这在单线程小Demo里完全没问题,但一旦场景复杂度上来,CPU单核处理不过来,帧率就会卡死在逻辑或者渲染最重的那个环节。
正确的思路是并行化:逻辑更新和渲染提交在不同线程上跑。逻辑线程(也叫主线程)负责游戏规则、物理、AI等;渲染线程负责把逻辑层产出的场景数据转换成绘制命令。这样两边都能各自在一个帧时间内做更多事。但要注意,并行是有代价的,两边之间的数据交接和同步机制,就是架构设计的核心难点。
2.2 帧缓冲机制:让CPU领先GPU一到两帧
跨线程并行还只是第一步,真正的关键是你得容忍"CPU比GPU快得多"这件事。假设渲染线程一帧花了5毫秒,GPU执行这些命令花了12毫秒,如果CPU每次都等GPU执行完再提交下一帧,那你的帧率就被GPU拖死了。反之如果GPU很闲,CPU提交跟不上,你又会浪费GPU能力。
业界普遍的做法是帧缓冲(Frame Buffering)机制:CPU侧可以提前准备2到3帧的渲染命令,GPU在执行第N帧的时候,CPU已经在准备第N+2帧了。对应的数据结构是帧缓冲区数组,每一帧轮流使用。
2.3 渲染线程、工作线程和命令列表的交接
这里涉及一个具体的架构决策:你的渲染提交是单线程一个命令队列,还是多线程多个命令队列最终合并?我直接说结论:现代引擎基本都走多线程提交,因为单线程命令生成的上限就是那样,瓶颈很明显。但多线程提交意味着你要解决大量并发写入的问题。
一个比较成熟的模型是这样的:
- 主线程(逻辑线程)遍历场景,产出渲染批次数据,写到帧级数据块里;
- 渲染线程拆出若干工作线程,每个工作线程持有独立的命令列表;
- 工作线程按不同的渲染队列(不透明、半透明、阴影、后处理)并行生成命令;
- 最后统一提交到GPU命令队列,或者按优先级依次提交。
这里的架构要点是:每个线程写自己的命令列表,最后合并,尽量不做跨线程共享可变数据。命令分配器按线程分配,Buffer上传按帧循环轮转,这样能大幅减少锁竞争。
2.4 一个简化的帧循环伪码
纸上谈兵没意思,我写一个简化版本的帧循环结构,我用C++风格的伪码表述,但逻辑对任何语言都适用:
// 每一帧循环 for (;;) { uint32_t frameIndex = frameId % NUM_FRAMES_IN_FLIGHT; // 主线程:等待上一帧的命令列表执行完成(或者至少等资源可用) gpuFence[frameIndex].Wait(); // 主线程:更新游戏逻辑,产出渲染数据到frameData[frameIndex] UpdateGameLogic(frameData[frameIndex]); // 渲染线程:并行提交工作 parallel_for_each(renderQueues, [&](QueueJob& job) { CommandList cmd = commandAllocators[threadId]->GetCommandList(); cmd.Begin(); job.GenerateCommands(frameData[frameIndex], cmd, resourceCache); cmd.End(); // 收集到提交队列 pendingCmdLists.Enqueue(cmd); }); // 提交线程:等待所有工作线程完成后,统一提交 pendingCmdLists.WaitAll(); commandQueue.ExecuteCommandLists(pendingCmdLists); // 发出Fence,表示这一帧的命令已提交,GPU完成后会触发 gpuFence[frameIndex].Signal(commandQueue); frameId++; }这只是骨架,但方向是对的:多线程产命令,单点提交,帧级同步。实际项目里,帧内还会拆成更细粒度的SubFrame阶段控制,但整体架构逻辑就是这样。
3. GPU同步语义:CPU和GPU握手的三条关键命门
帧缓冲机制跑起来之后,下一个问题就是:CPU和GPU之间到底怎么同步?这是渲染系统架构中最容易绕晕的一层,也是无数崩溃、花屏、卡顿的源头。
3.1 Fence、信号量和队列的基本模型
先明确几个概念。GPU有自己的执行队列,CPU往队列里塞命令,GPU按顺序一件件执行。CPU想要知道GPU某批命令执行完了,得靠Fence(围栏)或信号量。Fence的语义很简单:CPU往队列里插入一个Fence对象,GPU执行到这里时激活它,CPU可以阻塞等待Fence被激活。
这里有个架构层面的选择:你用阻塞等待还是非阻塞查询。我强烈建议在非关键路径上用非阻塞查询,在真正必须要等的地方用阻塞等待。比如等待某个Buffer被回读结果时,可以非阻塞轮询一帧内是否完成,而不是直接把CPU线程挂起几百微秒。
3.2 双缓冲资源与环形缓冲区:用版本号判断可用性
同步机制落地的最大难点,是资源的跨帧复用。比如你每帧都要传一批骨骼矩阵到显存,不可能每帧都新建Buffer,那样内存和带宽都不够。通常做法是维护一个环形缓冲区(Ring Buffer),帧与帧之间轮流使用不同的内存区域。
环形缓冲区的核心设计是版本化管理:每个区域记录一个帧ID,只有当GPU执行到对应帧之后,该区域才能被安全重写。这里的架构要求是,资源的"可用性"必须由生命周期系统统一管理,而不是靠程序员回忆哪一帧被占用。我看到太多项目把这种逻辑散落在各处,最后出了"这不是崩溃是花屏"的诡异问题。
3.3 Barrier(屏障)其实是同步的一部分
谈到GPU同步,很多人会忽略Barrier的重要性。Barrier是告诉GPU:接下来的某些编译操作,必须等之前某些资源操作完成才能开始。比如你把RenderTarget从"写入"状态切到"着色器读取"状态,中间如果没有Barrier,GPU可能读到旧数据。
这部分的架构设计,核心在于把Barrier的插入时机集中到渲染队列的调度层,而不是让业务代码到处调。我会在第五章详细讲资源状态管理,这里先提一句:Barrier不是引擎自动帮你搞定的东西,它是你要管理的一类一等公民资源。很多从OpenGL/老D3D11时代过来的开发者,一开始很不习惯现代API的显式Barrier,但一旦接受它,会获得极大的性能优化空间。
3.4 边界情况:回读与等待
有些功能(比如点击拾取、GPU裁剪结果)需要从GPU把数据读回CPU。这属于同步里最危险的操作,如果你不做处理,CPU会阻塞在等待GPU回写上,整个管线停摆。
架构上推荐的做法是"双缓冲回读":第一次提交读取请求,第二帧检查结果是否就绪,第三帧使用。用帧缓冲机制天然抵消阻塞时间,比任何实时回读方案都稳。这跟渲染架构的帧缓冲思路是一脉相承的。
4. 渲染资源管理架构:从引擎Asset到GPU内存
渲染系统架构的另一半,是资源管理。没有一套清晰的资源管理架构,渲染线程写得再漂亮,也撑不住复杂场景的资源加载、卸载和更新需求。
4.1 引擎资源与GPU资源分离的灰度设计
我推荐的一种架构是:引擎层持有"资源句柄"(Handle),底层持有真正的GPU资源对象。引擎资源的生命周期和GPU资源的生命周期分开管理,通过引用计数和帧ID来协调。
举个例子:美术加载了一个角色Mesh,引擎层创建了一个资源句柄,底层在GPU显存中建立了顶点缓冲和索引缓冲。当角色离开场景且引用被释放时,引擎层通知资源系统,资源系统等到GPU不再引用该缓冲的那一帧之后,才真正释放显存。这个"GPU不再引用"的判断,就得依赖帧缓冲同步。想省心,就把这个逻辑收敛到一个资源管理器里,让它统一处理延迟释放。
4.2 上传策略:静态资源、动态资源与流式资源
不同种类资源的GPU内存管理策略是完全不同的,我把它们画成三类,分别说明:
| 资源类型 | 典型例子 | 更新频率 | 推荐策略 |
|---|---|---|---|
| 静态资源 | 纹理、静态网格、着色器 | 加载后不再更新 | 加载时一次性上传,驻留显存 |
| 动态资源 | 骨骼矩阵、每帧常量、粒子参数 | 每帧更新 | 环形缓冲区的子区域分配 |
| 流式资源 | 大地图贴图、虚拟纹理页面 | 按需上传/回收 | 分块上传,按视口LOD维护优先级 |
静态和动态资源的边界要清晰:如果你把静态资源放进动态缓冲区,每帧无谓地上传一遍,带宽浪费巨大。如果你把一个动态资源当成静态资源访问,改数据时又要重建缓冲,性能一样难看。架构层的目录划分,一开始就要把这个分清楚。
4.3 资源状态与生命周期如何在帧缓冲中周转
动态资源跨帧周转,需要有一个"资源版本号"的概念。每一帧的资源缓冲都打上当前帧号,当GPU执行到某一帧后,系统会标记该帧所有相关资源都可以复用。我给所有动态Buffer做统一管理时,都会附带一个环形索引和帧号:
- 当前写位置永远指向
当前帧 % N; - CPU在写之前,先查该槽位的最近使用帧号是否小于GPU当前执行帧号;
- 如果GPU还没走完那一帧,就再开一个更大的环或者强制等待。
这套机制写好了,动态资源上传几乎是零成本。写不好,你会在各种"数据被覆盖导致闪烁"的问题里沉浮。
4.4 内存预算与预算预警
渲染系统特别讲究显存预算。贴图动不动几百MB,Mesh顶点动辄几十MB,一旦爆显存,驱动开始把显存放回内存,性能会雪崩。架构层面要内置一个内存预算管理器,统计每种资源类型的显存占用,设置软硬告警线。这一块对主机平台尤其重要,PC上也要做,否则你就会收到美术同学各式各样的"为什么场景一卡一卡的"反馈。
5. 可见性判定与GPU驱动渲染:减少无效工作的架构手段
裁剪(Culling)是渲染系统里收益最高的优化手段。它做得好的话,把场景里三分之二甚至更多的绘制提交在进入渲染管线之前就消掉,性价比极高。
5.1 为什么传统八叉树不够了
传统方案是CPU维护一颗动态场景树(八叉树或BVH),每帧遍历树剔除不可见物体,收集需要绘制的Mesh列表。这个方案逻辑清晰,但有一个天然瓶颈:CPU遍历场景树本身要花时间,当场景包含几十万个物件时,CPU裁剪的开销会反超GPU省下的时间。
现代引擎的应对方向是"缓冲剔除"(Batched Culling):把物体包围盒数据上传到GPU,让GPU用Compute Shader做粗粒度剔除,再把剔除结果写回GPU显存,供后续DrawCall做实例化。这一套称为GPU Driven Rendering。
5.2 GPU Driven Rendering的引入
GPU Driven Rendering的架构思路很简单:你不把所有物体当成单个DrawCall提交,而是把所有物体的位置、包围盒、材质ID聚合到几个大Buffer里,再让GPU自己决定哪些物体可见,并用间接绘制(Indirect Draw)参数来驱动真正的绘制命令。
这条架构路线在TBDR移动GPU和PC上都吃得开,因为它的核心就是"把决策从CPU搬到GPU"。代价是什么呢?调试复杂度极高。因为通过间接参数指定的绘制调用,你在CPU侧看不到具体的绘制目标,必须依赖GPU调试工具保存回读信息。
5.3 裁剪结果回读的策略
即使是GPU Driven方案,有些时候你还是需要把裁剪结果拿回CPU。比如你需要知道"玩家视野里到底有多少物体"来决定某些LOD策略,或者需要做动画系统的按需更新。回读策略我前文讲过:用双缓冲加非阻塞查询,把芯片的实时回读成本降到最低。
具体到架构设计上,裁剪结果本身应该独立存放,不能跟绘制命令绑在一起。这样CPU回读和GPU绘制之间,不会互相阻塞。这个细节看似很小,但对整体管线的流畅度影响很大。
5.4 对架构的影响:场景结构扁平化
采用GPU Driven思路后,场景管理会从树状结构转型为更扁平的实例列表结构。树状结构用来做CPU侧的粗粒度管理(资源加载、编辑器选择),渲染专用的数据则统统搬进一个流式的InstanceBuffer里。这样的好处是渲染线程非常容易并行化:每个工作线程只处理一块实例缓冲,不用担心树遍历的顺序依赖。
我个人的体验是:第一次从树状渲染切到InstanceBuffer额外绘制的项目,管线性能提升通常30%起,而且后续加特性也轻松得多。这个方向值得推荐。
6. 绘制队列、状态合并与批合并的调度细节
前面几层架构解决的是"渲染系统能不能跑起来、并行度有多高"的问题。真正决定渲染系统最终效率的,还有一层很细节的设计:渲染队列怎么排、状态怎么合、绘制调用怎么批。
6.1 绘制队列的排序与分类问题
每一帧你会收集到一堆物体,它们有各自的材质、深度状态、光照通道。如果毫无顺序地提交,渲染状态切换会非常频繁,GPU和CPU都要承担额外开销。架构上常见的做法是维护几类队列:
- 不透明队列:按材质排序,尽量减少Shader切换;
- 半透明队列:按深度从后到前排序;
- 天空盒/背景队列;
- 阴影相关队列;
- 后处理队列。
半透明队列从后往前排序,是为了保证正确的混合效果。这个大家都知道,但实际实现时真正麻烦的是:半透明物体之间如果存在互相遮挡,排序无论如何也不可能完美。架构层面的缓解手段是把半透明物体拆成很薄的外包围盒排序,或者接受轻微的深度穿插,再用排序偏移来润色。这种取舍,必须写进架构文档里,不然每次优化都会有人绕回去。
6.2 状态缓存的实现:从状态块到状态比较
在底层API层级,切换渲染状态(管线状态对象)的开销往往没那么大,尤其在D3D12和Vulkan时代,创建Pipeline State Object(PSO)的成本主要在构建时,切换时的成本主要是驱动内部的校验。但CPU侧生成绘制命令时,如果每条命令都重复设置一遍完整状态,命令列表的体积会爆炸,提交带宽也会浪费。
所以我建议在渲染线程内做一个"状态缓存层":它比较当前提交命令所需的状态块与最近一次已提交的状态块,相同则跳过,不同才写入切换命令。这个缓存层可以是简单的哈希比较,也可以是逐字段比较,关键是它要把"命令生成"和"实际提交"解耦。
6.3 绘制调用流水线的一个典型示例
实际代码调度上,我更推荐下面这个流水线化提交逻辑:
- 渲染线程先遍历各类队列,把每个绘制请求转换成"最小绘制单元"(Mesh + Material + Transform + Lightmap信息);
- 状态缓存层对最小绘制单元做静态排序和状态归类;
- 对于相同Mesh、相同状态且支持实例化的物体,直接生成一条实例化绘制命令;
- 对于动态对象的矩阵数据,批量写入动态Buffer环形区;
- 工作线程并行生成命令列表,最后提交。
一套走下来,DrawCall数量能压缩一个数量级。我记得一个中等复杂度的场景,优化前3000多次提交,优化后500次不到,帧耗时直观地降了三分之一。
6.4 我踩过的批合并坑
批合并看起来简单,实际坑很多。最典型的坑是:美术同学喜欢给同一种材质挂不同纹理,Unity和UE里都有"材质实例化"机制,结果批合并直接被破坏,又不敢乱动。架构上应该做的是"参数化材质"而不是"实例化材质",把所有可变化参数收敛到一张Buffer里,让材质本身保持唯一。这一步架构做得好,批合并的稳定性会有质的飞跃。
然后是半透明排序的问题。批合并几乎跟半透明排序天然冲突,因为融合排序要求严格按深度顺序,批合并会打乱这个顺序。我的推荐是半透明队列不强行批合并,保留更细粒度的排序,哪怕多几十个DrawCall,也远好过为了批合并导致混合错乱。
7. 渲染系统架构设计里的几个硬性原则
架构不是蓝天白云的图纸,它是无数个"不行""禁止""必须"约束出来的。这一章我分享几条自己长期坚持的硬性原则,算是我踩过大量坑之后沉淀下来的纪律。
7.1 原则一:CPU侧尽量不访问GPU内存对象
我见过很多渲染架构,CPU侧需要改一个顶点数据就直接map一个GPUBuffer,改完立刻提交。这在API层面是合法的,但它会引入至少一次同步等待或一次数据拷贝。正确做法是:CPU写到一个CPU可见的暂存Buffer,渲染线程在提交时做一次GPU队列间的拷贝或绑定。
换句话说:GPU内存对象只读,CPU更新只走暂存缓冲。这会让命令提交路径变得非常朴素,几乎没有隐式同步。
7.2 原则二:所有渲染状态变更必须集中
前文提过这条,我要再强调一遍。渲染状态包括:管线状态、资源绑定点、顶点布局、深度模板、视口裁剪区等。所有这些状态的变更必须在渲染线程的提交循环里完成,任何其他线程(逻辑、表现、UI)都不允许绕过。如果你发现某段代码在别的线程里直接改了绑定资源,那一定是架构违规。
现实中这条原则最大的挑战不是技术,而是架构执行力。尤其在多团队协作时,总有人觉得"我改一条绑定点不至于吧",管理成本会很高。我的经验是,在调试构建里给资源绑定加只读检测,一发现跨线程修改,直接断言报崩溃,把问题掐死在源头。
7.3 原则三:帧内数据块按帧写,不做跨帧共享可变
跨帧共享可变数据是渲染系统大忌,因为它会破坏预测性。举一个我曾遇到的例子:某个贴花系统在帧N收集到一堆贴花请求,写进了"全局列表";帧N+1的渲染线程读取这个列表时,逻辑线程可能正在往里面追加新条目,于是渲染到的场景数据忽多忽少,画面出现随机性闪烁。最终的修复方式是贴花请求数据全部复制到帧级数据块里,每帧一份,渲染线程永远只读当前帧的数据。
这条原则贯彻到底,多线程同步的问题会指数级减少。
7.4 原则四:观察帧缓冲的并行度,而不是只看帧率
性能调优时大家喜欢看帧率,但帧率只是结果。我推荐的指标是看"CPU提交时间""GPU执行时间""等待时间"三个值。如果CPU提交时间比GPU执行时间短很多,那瓶颈在GPU,你需要优化渲染算法;反之瓶颈在CPU,你需要优化提交逻辑。两个数值重合且都不短,矩阵调整下。这套诊断思路对渲染架构设计极其重要,因为它直接告诉你下阶段应该投资在哪。
8. 架构演进路线:从固定管线到现代渲染架构
最后聊聊架构本身怎么演进。几乎没有一个引擎是从零一步到位写成现代渲染架构的。实际上大多数项目都是从固定管线、简单提交、单线程渲染逐步演进来的。演进路径有规律可循。
8.1 第一步:先分离渲染线程
把渲染提交从逻辑线程中拆出来,是收益最大且风险最低的第一步。逻辑线程产出场景数据,渲染线程生成命令,中间用帧缓冲数据隔离。改动集中在这两层接的文件,其他模块几乎不受影响。
8.2 第二步:再把资源管理系统化
单线程改多线程之后,第一波不适就是资源管理。你会发现动辄有资源还没上传就被引用、资源被多个帧共用等混乱问题。这一步把引擎资源与GPU资源的生命周期统一打理,延迟释放机制就位,大概率能稳定运行。
8.3 第三步:引入GPU Driven和并行提交
如果项目已经迈过前两步,性能需求还压着你,那么下一步就是做GPU Driven渲染、并行命令列表、间接绘制这些高阶手段。这一步涉及面广,最好由资深渲染工程师牵头做,因为调试难度在那里摆着。
8.4 最后:建设调试与性能分析工具链
最后一步反而是最容易被忽视但最不能省的:工具链。没有一套强大的调试工具链,你无法定位GPU内部的性能瓶颈,也无法在退化的渲染结果里找出哪一步改错了。帧捕获器、PSO分析、GPU计时器、资源生命周期追踪工具,这些投入都是值得的。
我记得自己在一款跨平台项目里把渲染系统从单线程D3D11架构迁到多线程Vulkan架构,经历了一年多。回过头看,最困难的从来不是API本身,而是架构上那些"看不见的连接":资源生命周期、同步语义、数据隔离。把这些连接理清楚,渲染系统才能真正谈得上"架构"两个字,否则充其量只是一堆渲染函数的集合。
这篇文章更多是梳理思路。如果你正在做渲染系统架构设计,我建议你先把自己的线程模型画在纸上,把帧缓冲和数据隔离标清楚,再去碰具体的API。架构不过是提前把未来的复杂度安排好,渲染系统尤其如此。