《游戏引擎实现原理》1.2 渲染器的分层:RHI / 渲染器 / 场景
上一节看到 CPU 与 GPU 是两条流水线,那渲染器该怎样分层来管好它们?本节把渲染器拆成 RHI(硬件接口)、渲染器(帧组织)、场景(数据)三层,给出 helio 的目录与接口草图,并用一个可运行的依赖方向守卫,把「只能向下、不能成环」这条铁律钉成编译器与测试都能把关的断言。
tag
上一节看到 CPU 与 GPU 是两条流水线,那渲染器该怎样分层来管好它们?本节把渲染器拆成 RHI(硬件接口)、渲染器(帧组织)、场景(数据)三层,给出 helio 的目录与接口草图,并用一个可运行的依赖方向守卫,把「只能向下、不能成环」这条铁律钉成编译器与测试都能把关的断言。
渲染器的第一课不是画三角形,而是搞清楚一帧里 CPU 和 GPU 谁在等谁。本节用离散事件模型模拟 CPU 提交与 GPU 执行的流水线,算出单/双/三缓冲下的稳态帧率与延迟,再在真实 WebGPU 设备上量出 CPU 提交与 GPU 排空的耗时,用数字说明为什么「CPU 帧」与「GPU 帧」不是同一件事。
本导航汇总 content/posts/game/engine 下的引擎相关文章,本目录直接收录 2 篇,另有 defold 子目录收录 2 篇,合计 4 篇。内容聚焦一个很实际的问题:新手或小团队到底该从哪个引擎开始。这里既有一线引擎的横向对比,也有对 Unreal Engine 的入门级介绍,还有 Defold 这类轻量引擎在脚本语言选择上的边界讨论。本页给出主题速览、子目录入口与分角色阅读建议,适合正在选引擎、准备迁移或评估轻量方案的同学按需取用。
本导航收录 content/posts/game/engine/defold 下的 2 篇文章,全部围绕轻量游戏引擎 Defold 的脚本语言选择展开。一篇系统梳理在 Defold 中使用 Lua 写游戏逻辑的优势与局限,另一篇回答一个被反复问到的问题:能不能用 Python 来做 Defold 开发。两篇合起来正好界定了 Defold 的能力边界与协作约束,适合评估轻量引擎是否适配自己团队、或者已经在用 Defold 却对技术栈有疑问的开发者快速定位答案。
《游戏引擎实现原理》卷一附录 C:汇总全书性能基准的原始数字与测量方法。给出 SoA vs AoS、连续遍历 vs 指针追逐、对象池 vs new 三组实测数据(含中位数、堆增量与 IQR),讲清进程隔离、预热、hrtime 计时与 21 样本中位数统计这套方法论,并诚实声明这些是同机相对量级、绝对数值不可移植。
《游戏引擎实现原理》卷一附录 A:把全书出现的 helio 内核类型与关键 API 按 core / ecs / jobs / reflect / platform 五个模块整理成速查表,每个条目给出签名、一句话说明与所在章节,并附模块依赖方向、典型组合用法与按章反查索引,方便边读边查、随时回跳。
卷一《引擎内核》的收尾:逐章回顾 18 章各自解决了什么问题、它们如何拼成一台能跑的引擎,给出从零到能跑的施工顺序与各阶段验收标准,并说明进入卷二《渲染引擎》的路线——GPU 抽象、渲染图、相机与资源,以及内核与渲染之间那道接缝的三个具体例子。
用装好的 helio 内核跑出《微光岛》第一个能走动的方块:一份 120 帧预录输入序列驱动方块在 16×16 岛屿平面上按固定步长移动,连跑两遍,最终位置与轨迹校验和逐位相同;正文给出真实的最终坐标与断言结果,并解释那个 9.7e-17 的浮点残渣从何而来。
把前面 17 章的零件——arena 帧内存、slot map 句柄、ECS 世界、固定步长主循环、子系统注册表——装配成一个可运行的 helio 内核 Kernel 类,真跑 4 帧,打印每帧子系统的执行顺序与真实耗时,并说清装配顺序为什么必须是拓扑序。
存档格式会变:今天的 hp 明天可能改名 health。本节给 helio 内核的 schema 迁移框架:存档里带 $v 版本号,加载时沿迁移链逐级升到当前版本,缺环直接抛错、绝不静默降级。实跑一份 v1 存档升到 v3,字段从 {hp, name, gold} 变成 {health, name, mana, level}。
朴素 JSON.stringify 遇到父子互指的图会抛 TypeError,报错原文是「Converting circular structure to JSON」。本节给出 helio 内核里带 id 表的对象图序列化器:把对象引用替换成整数 id,循环变成两条指向同一 id 的边,两趟重建后引用同一性保持不变;句柄部分复用第 6 章的世代句柄。
第 16 章的类型描述符在这一节接上序列化器:同一张字段表既能写出可读 JSON,也能写出紧凑二进制。给出 helio 内核里可编译的 TypeRegistry、json-serde 与 binary-serde 三个完整文件与 vitest 用例,并实测同一个 Player 对象 JSON 93 字节、二进制 37 字节。
不用装饰器、不依赖 emitDecoratorMetadata:用一个构建期脚本扫描源码里带标记的类型定义,生成一份显式注册表 .ts 文件,再由运行时 import 进注册表。本节真跑一次扫描,把扫过的文件数、抽出的类型数与属性数、生成的代码行数都写进正文。
从零实现 helio 内核的 TypeRegistry:用 defineType 按字段类型累加偏移,产出名字、类型、偏移、默认值四件套的属性描述符;注册顺序被保留,遍历稳定可复现。本节给出完整可编译实现与 vitest 用例,并真跑一次 registry.get('Vec3').properties,把真实输出贴进正文。
编辑器要列出属性、存档要读写字段、脚本要绑定对象——这三件事都要求引擎在运行时知道「类型里到底有什么」。本节讲清这份类型信息为什么必须有,交代 Node 26 只做类型剥离、装饰器元数据走不通这个硬约束,并把它讲成一个主动的设计选择:显式注册表。
任务被派发到别的线程之后,帧边界就成了同步点:用 Atomics 屏障让所有线程在每帧末尾对齐,用双缓冲让逻辑线程与渲染线程各写各的、只在帧边界交换一次引用。本节用 worker_threads + Atomics 跑一个最小同步 demo,并标出命令行不可复现的部分。
每帧重建一张任务图:把本帧真实出现的任务(可见区块数每帧都在变)拼成 DAG,跑一次拓扑分层,得到本帧的执行序与并行批次。本节给出可编译的 FrameScheduler,并真实跑出 5 帧的执行顺序、每帧并行度,以及「重建」本身随任务数增长的开销。
把帧内任务的先后关系表达成一张有向无环图,再用 Kahn 算法做分层拓扑排序,输出一个合法执行序与每层可并行的任务集合。本节给出 helio 内核可编译的 TaskGraph 完整实现与 vitest 用例,并用一个真实的环断言「循环依赖被检测到并报错」。
14.2 的共享队列在核多时会撞上锁瓶颈。本节实现 Chase-Lev 双端队列:每个 worker 一条 deque,拥有者从 bottom 端 LIFO 取,窃取者从 top 端 FIFO 偷,只剩最后一个元素时用 Atomics.compareExchange 定胜负。并用 8:1 的不均衡负载,实测静态分片与 work stealing 的 makespan 与处理数分布。
从零写一个基于 worker_threads 的线程池:固定 4 个 worker、一条用 SharedArrayBuffer 装的共享环形任务队列,用 Atomics.compareExchange 做锁、Atomics.wait 挂起空闲 worker。给出 JobSystem 实现与 vitest 用例,实测 1000 个任务串行与并行的差距,以及任务太碎反而慢近 11 倍的反例。