中EN
概念页

自定义内存分配器

Custom Memory Allocators

内存从两方面影响性能:动态分配(malloc / 全局 new)很慢,以及访问模式主导现代 CPU 的性能。GEA 的经验法则是「尽量少做堆分配,绝不在紧密循环里做堆分配」,并介绍游戏引擎常用的自定义分配器:栈分配器(含双端栈)、池分配器、对齐分配、单帧与双缓冲分配器;以及如何用栈/池避免碎片,或用碎片整理加指针重定位(智能指针、句柄)解决碎片。

为什么堆分配慢(卷 I 6.2、6.2.1)

  1. 通用堆分配器要处理 1 字节到 1 GB 的任意请求,管理开销大;
  2. 多数操作系统上 malloc() / free() 要从用户态切到内核态再切回,上下文切换极其昂贵(用户态/内核态见 进程、线程与纤程)。
    • 原书问题:「每次 malloc()/free() 都要切入内核」不符合现代通用分配器的实际。以 glibc malloc 为例,绝大多数请求在用户态由线程本地缓存 (tcache)、fastbin/smallbin 等空闲链表满足,只有需要扩展堆(sbrk)或大块请求(超过动态阈值时直接 mmap)时才进行系统调用;free() 通常也只是把块放回用户态的链表,并不把内存还给操作系统(glibc wiki「MallocInternals」)。Windows 堆、jemalloc、mimalloc 等同样如此。通用堆慢的主要原因是第 1 条的通用性开销、加锁/竞争、缓存不友好与碎片,而非每次都陷入内核;GEA 的经验法则本身仍然成立。 GEA 推荐延伸阅读 Christian Gyrling 关于自定义分配器的博客文章(GEA 参考文献 [194])。自定义分配器更快的两个原因:从预分配的内存块(用 malloc/new 分配或声明为全局数组)中满足请求,全程在用户态;并且可以对使用模式做假设,比通用分配器高效得多。另一方面,数据放在小而连续的内存块中,CPU 处理起来远比分散的数据快(见 计算机硬件与内存层次、数据局部性)。

栈分配器(卷 I 6.2.1.1)

  • 适用:关卡加载时分配、关卡期间几乎不分配、关卡结束整体释放这种栈式模式。
  • 实现:一大块连续内存(malloc/全局 new,或全局字节数组——此时内存实际来自可执行文件的 BSS 段)加一个栈顶指针;分配就是把指针上移请求的字节数。
  • 只能按分配的逆序释放。简单的强制办法是不允许单独释放,而是提供 getMarker() 返回当前栈顶标记、freeToMarker(marker) 回滚到标记处(必须回滚到两个块的边界,否则新分配会覆盖最上面块的尾部),以及 clear() 全部清空。
  • 双端栈分配器:同一块内存,一个栈从底部向上分配,一个从顶部向下分配,两者的用量可以此消彼长,只要总量不超过整块即可。Midway 的街机游戏《Hydro Thunder》全部内存都由一个双端栈管理:下栈加载/卸载关卡(赛道),上栈分配每帧申请和释放的临时块;这套方案效果极好,游戏从未遇到内存碎片问题(首席工程师 Steve Ranck 对此有深入介绍,见 GEA 参考文献 [9] 第 1.9 节)。

池分配器(卷 I 6.2.1.2)

  • 适用:大量同尺寸小块的分配和释放,如矩阵、迭代器、链表节点、可渲染网格实例。
  • 预分配一块大小恰为元素尺寸整数倍的内存(如 4×4 float 矩阵每个 64 字节),所有元素串成空闲链表;分配取链表头,释放挂回链表,都是 O(1)O(1)。
  • 把 next 指针存放在空闲块自身里:空闲块本来就没被使用,所以不浪费内存,前提是 elementSize >= sizeof(void*)。若元素小于指针,用元素索引代替指针,如 16 位整数池用 16 位索引,前提是元素不超过 216=65,5362^{16}=65{,}536 个。

对齐分配(卷 I 6.2.1.3)

  • 对齐要求回顾(见 C++ 内存布局与对齐):32 位整数/浮点 4 字节对齐(地址末位 0x0/0x4/0x8/0xC),128 位 SIMD 向量通常 16 字节对齐;PS3 上经 DMA 传给 SPU 的块应 128 字节对齐以获得最大吞吐。所有分配器都必须能返回对齐的内存。
  • 思路:多分配一点,把地址上移到对齐边界再返回。多数实现额外分配「对齐值减一」(L−1L-1) 字节,这是最坏情况的偏移(例如要 16 字节对齐而原地址末位是 0x1,需上移 15 字节)。书中第一版 AllocAligned() 就是 new U8[bytes + align - 1] 后直接返回对齐指针——但这样无法正确释放(丢失了原始地址),因此才有下面存放偏移的改进版。对齐地址的位运算: aligned=(addr+(L−1)) & ∼(L−1)\text{aligned}=(\text{addr}+(L-1))\ \&\ \sim(L-1) LL 必须是 2 的幂,L−1L-1 的低 N=log⁡2LN=\log_2L 位全为 1,取反后与地址按位与即去掉低 N 位。
  • 释放时找回原地址:把偏移量 (shift) 存在对齐地址前一个字节 p[-1] 里。为保证总有空间存放,分配 LL(而不是 L−1L-1)个额外字节,并且即使原地址已对齐也上移整整 LL 字节,于是偏移在 1 到 LL 之间。一字节可存到 128 的偏移;由于偏移永不为 0,可把 0 解释为 256,从而支持到 256 字节对齐;更大的对齐需要更宽的头部。
void* AllocAligned(size_t bytes, size_t align) {      // align 必须是 2 的幂
    U8* pRaw = new U8[bytes + align];
    U8* pAligned = AlignPointer(pRaw, align);
    if (pAligned == pRaw) pAligned += align;          // 总是留出存放偏移的空间
    ptrdiff_t shift = pAligned - pRaw;                 // 1..256
    pAligned[-1] = static_cast<U8>(shift & 0xFF);
    return pAligned;
}
void FreeAligned(void* pMem) {
    if (!pMem) return;
    U8* pAligned = reinterpret_cast<U8*>(pMem);
    ptrdiff_t shift = pAligned[-1]; if (shift == 0) shift = 256;
    delete[] (pAligned - shift);
}

(据书中代码精简)

单帧与双缓冲分配器(卷 I 6.2.1.4)

  • 单帧分配器:一个栈分配器,每帧开始 clear()。分配的内存永远不用释放,而且极快。代价是纪律:内存只在本帧有效,绝不能跨帧缓存指向它的指针。
  • 双缓冲分配器:两个等大的单帧栈每帧轮换(swapBuffers() 后 clearCurrentBuffer()),第 i 帧分配的内存在第 i+1 帧仍有效。
  • 典型用途:在多核主机(Xbox 360/One、PS3/PS4)上缓存异步任务的结果——第 i 帧发起任务并给它一个从双缓冲分配器拿的目标缓冲,任务在第 i 帧结束前写完;第 i+1 帧缓冲交换,结果位于非活动缓冲中不会被覆盖,只要在第 i+2 帧之前用掉即可(任务系统见 多线程渲染与任务系统)。

内存碎片(卷 I 6.2.2)

  • 随机顺序、不同大小的分配与释放会让堆变成已用块与空闲「洞」交错的拼布;洞多且小时就是碎片化。问题在于分配必须连续:两个 64 KiB 的洞凑不出一个 128 KiB 的请求,即使总空闲字节够也会失败。
  • 虚拟内存把不连续的物理页映射成连续的虚拟地址,并可把不常用的页换到硬盘,碎片问题较轻。GEA 指出 PS5 与 Xbox Series X/S 支持虚拟地址重映射,但都不像 Linux/Windows 那样自动把虚拟内存换页到磁盘,所以主机开发者面对的是固定数量的 RAM,要流式进出数据必须自己实现流送系统。
  • 用栈和池避免碎片:栈分配总是连续、且按逆序释放,天然免疫碎片;池会产生碎片,但因为所有块等大,永远不会因为找不到足够大的连续块而提前耗尽内存。

碎片整理与重定位(卷 I 6.2.2.2)

  • 大小不一、顺序随机的分配无法用栈或池时,可以定期碎片整理:把高地址的已分配块下移填洞,洞逐渐「冒泡」到高地址,最终合并成一个大洞。简单算法:找到第一个洞,把紧挨在它上面的块下移到洞的起点,重复。
  • 难点不在移动,而在指针失效:必须修正所有指向被移动块的指针,即指针重定位 (pointer relocation)。由于没有通用办法找出所有指向某区域的指针,要么手动追踪所有指针,要么改用更易重定位的东西:
    • 智能指针:行为像指针的小类;例如让所有智能指针加入一个全局链表,块移动时扫描链表修正指向该块的指针。
    • 句柄 (handle):通常是指向一张不可重定位的指针表的索引;块移动时只需更新表中的指针,句柄值本身永不改变,持有句柄的对象完全不受影响。
  • 不可重定位的块(如不使用智能指针/句柄的第三方库):最好让该库从可重定位区域之外的专用缓冲分配;或者接受少量不可重定位的块——数量和尺寸都小时重定位系统仍然表现良好。
  • Naughty Dog 的所有引擎都支持碎片整理:尽可能用句柄;无法避免的原始指针被仔细追踪并手动重定位;少数游戏对象类因各种原因不可重定位,但数量很少、尺寸相对可重定位区域极小,不造成实际问题。
  • 摊销:不必一次整理完,每帧最多移动 N 个块(N 如 8 或 16)。30 帧/秒时,堆通常不到一秒就能整理完而不影响帧率;只要分配释放速度不超过整理速度,堆始终基本无碎片。前提是单个块足够小,移动一块的时间不超过每帧的预算;大块可拆成可独立移动的子块。Naughty Dog 只对动态游戏对象做重定位,它们从不超过几 KiB,通常小得多。

选型速查

分配模式推荐注意
关卡级、整体释放栈分配器 / 双端栈只能按逆序或回滚到标记释放
大量同尺寸小对象池分配器元素小于指针时用索引链表
SIMD、DMA 等对齐需求对齐分配偏移存于前一字节,最大 256 字节对齐
每帧临时数据单帧分配器指针不得跨帧
异步任务结果下一帧用双缓冲分配器在 i+2 帧前用完
大小不一、顺序随机通用堆 + 摊销碎片整理需要句柄 / 智能指针 / 手动重定位

PBRT 的视角:渲染器里的暂存缓冲与多态分配器(PBRT 4e 1.3.4、1.5.6)

  • ScratchBuffer:积分器为每条光线计算 BSDF 等临时对象时会产生大量小分配,通用分配器(如 new)要在多线程下维护复杂的空闲内存结构,可能占掉相当比例的计算时间。pbrt 的 ScratchBuffer 是一小块预分配内存,分配只是把偏移加上请求大小;不能单独释放,只能一次全部释放(重置偏移)。每个像素样本算完就 Reset()。它不是线程安全的,所以用 ThreadLocal 为每个线程按需各建一个。这与 GEA 的栈分配器/单帧分配器(见上文)是同一思路,只是生命周期从"一帧"缩短到"一个像素样本"。
  • Allocator:场景对象几乎都通过 Allocator(即 C++17 std::pmr::polymorphic_allocator)创建,常用 allocate_bytes()、allocate_object()、new_object() 及对应的释放函数。好处:便于统计总内存、可以换成适合大量小分配的分配器(构建加速结构时)、可以把场景放进 GPU 可见的内存。
  • 坑:标准容器的分配器在构造后就固定了,容器之间赋值(包括移动)不会换分配器,所以 pbrt 的构造函数常在成员初始化列表里就把分配器传给容器,哪怕暂时还不填数据。

PBRT 的视角:ScratchBuffer 与驻留(PBRT 4e B.4)

  • ScratchBuffer 是竞技场 (arena) 分配器:从一大块预分配内存中按对齐要求推进 offset 分配,不单独释放;Reset() 通常只把 offset 清零,不调用析构函数。空间不够时换更大的块,旧块暂存在 smallBuffers,到 Reset() 时归还系统。它不支持多线程共享,每个线程通过 ThreadLocal 各持一个;类用 alignas 对齐到缓存行以免伪共享。用于路径追踪中动态创建的 BxDF、BSSRDF、RayMajorantIterator。
  • 驻留 (interning):InternCache 对相同对象只存一份,用互斥锁支持并发 Lookup(),返回共享指针,因此可以用指针比较代替值比较;pbrt 对场景描述中的变换和场景实体中的字符串(InternedString)这样处理,复杂场景能省下大量内存。
  • 其他容器:InlinedVector(短向量不做动态分配)、自带 HashMap(标准库 unordered_map 要求元素指针在扩容后不变,因此每个元素都要单独分配;且 GPU 代码也要能用)。
  • TaggedPointer:64 位指针的高位实际不用于寻址(常见 48 位、新近 57 位地址空间),pbrt 把类型标签放在高 7 位,Dispatch() 用一个 switch 完成动态分发(每组最多 8 种类型,超出部分递归处理),可在 GPU 上替代虚函数。
  • 原书问题(笔误):B.4.5 "It offsets a few constructors" 应为 "offers"。
  1. Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 6 章 6.2 节
  3. PBRT 4e source excerpt: chapter 01 (Introduction)
  4. Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)第 1 章 1.3.4、1.5.6 节,pbr-book.org/4ed/Introduction
  5. PBRT 4e source excerpt: chapter B (Utilities)
  6. Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)附录 B B.2–B.7 节与延伸阅读,pbr-book.org/4ed/Utilities

相关页面

游戏开发C++ 内存布局与对齐C++ Memory Layout & Alignment理解 C/C++ 程序在内存中的样子,是写高性能引擎代码的前提。主要内容包括:声明与定义、内部与外部链接;可执行映像的几个段(text、data、BSS、rodata)以及栈和堆;结构体成员的自然对齐和填充;继承和虚函数如何改变类的布局(vpointer 和 vtable)。GEA 也简要回顾了面向对象:多重继承容易产生致命菱形,所以通常只允许 mix-in;组合往往比继承更好。游戏开发计算机硬件与内存层次Computer Hardware & Memory Hierarchy从冯·诺依曼结构讲起,依次是 CPU 的组成(ALU、VPU、寄存器、控制单元、时钟)、总线与字长、机器语言、汇编和寻址方式,再到内存体系:内存映射、虚拟内存和 TLB,以及为了缩小“内存鸿沟”而设计的寄存器文件、多级缓存(缓存行、标签、组相联、替换和写策略、一致性协议)和 NUMA(PS3 的 SPU 本地存储、PS2 的 scratchpad)。对游戏程序员最重要的结论是:数据要连续、紧凑、顺序访问;热循环的代码要小。游戏开发进程、线程与纤程Processes, Threads & Fibers并发编程依赖操作系统提供的服务:内核态与用户态、中断、系统调用、抢占式多任务。进程只是为线程提供运行环境(虚拟地址空间与资源),线程才是操作系统的基本执行单位(调用栈 + 寄存器)。线程由内核抢占调度;纤程 (Fiber) 由程序协作调度,适合实现引擎任务系统;用户级线程与协程完全在用户空间切换上下文,更轻量。游戏开发子系统启动与关闭Subsystem Start-Up & Shut-Down游戏引擎由许多相互依赖的子系统组成,启动时必须按依赖顺序初始化(B 依赖 A,则先启动 A),关闭时通常反序。C++ 全局/静态对象的构造与析构顺序不可预测,「首次调用时构造」的函数静态单例也无法控制析构顺序且时机不可控。GEA 推荐最朴素的方案:单例管理器的构造/析构函数什么也不做,提供显式的 startUp() / shutDown(),在 main() 里按顺序手动调用。游戏开发容器与哈希表Containers & Hash Tables容器的职责都是容纳和管理零个或多个元素,差别在于实现方式与取舍。GEA 概览了常见容器与操作、迭代器、大 O 复杂度与内存布局的综合考量,讨论引擎为何常自研容器、C++ 标准库 / Boost / Folly / Loki 的利弊,并重点讲了动态数组的增长策略与哈希表(开放哈希与封闭哈希、哈希函数、线性/二次探测、Robin Hood 哈希)。游戏开发数据局部性Data Locality数据局部性 (Data Locality) 是一种优化思路:CPU 读取内存时会把相邻的一整条缓存行一起载入,所以把数据按处理顺序连续存放,就能大幅减少缓存未命中。具体手段包括:组件按类型存进连续数组,不再到处跟随指针;把活跃对象排在前面;把冷热数据拆开。代价是要放弃一部分继承、接口与指针带来的抽象。游戏开发SIMD 向量化SIMD VectorizationSIMD(单指令多数据)用一条机器指令并行处理多个数据。x86 上从 MMX 发展到 SSE(128 位,4 个 float)、AVX(256 位,8 个)和 AVX-512(512 位,16 个)。GEA 用 SSE 内建函数演示:数组相加、点积、向量×矩阵、矩阵×矩阵,强调两点——避免寄存器内的水平相加(先转置数据),以及把 SIMD 寄存器看成 N 条并行的「通道 (lane)」而不只是一个 4D 向量;条件分支则用向量谓词化 (vector predication / select) 代替。游戏开发 · UE5智能指针与 TObjectPtrSmart Pointers & TObjectPtrUE 有两套对象寿命管理:普通 C++ 对象用引用计数智能指针(TSharedPtr/TSharedRef/TWeakPtr/TUniquePtr),UObject 由垃圾回收管理,不能放进这些智能指针。UE5 新增的 TObjectPtr 用来替代 UPROPERTY 里的 UObject 裸指针:编辑器构建中提供动态解析和访问跟踪,非编辑器构建中与裸指针表现相同。游戏开发多线程渲染与任务系统Multithreaded Rendering & Task SystemsCPU 时钟频率约在 2003 年停在 3.4 GHz 附近后,性能来自多核。应用阶段的优化一是让代码与内存访问更缓存友好,二是利用多核:多处理器流水线(时间并行,提高吞吐但增加延迟)、并行处理(空间并行,延迟更低)以及两者结合的任务系统;图形 API 用命令缓冲让多个线程并行录制绘制命令,新一代 API(DirectX 12、Vulkan、Metal)把更多责任交给应用以降低驱动开销。游戏开发字符串与唯一标识符Strings & Unique Identifiers字符串在 C/C++ 中只是字符数组,变长意味着要么硬编码长度上限、要么动态分配;比较和复制都是 O(n)。引擎内部大量用字符串做资源名和对象 ID,处理不当会严重拖慢游戏。GEA 的建议:运行时代码尽量避免字符串类,若用则了解其成本并按引用传递;路径类是值得专门做的例外;对象与资产的唯一标识用哈希字符串 ID(string id),兼得字符串的可读性与整数的比较速度。游戏开发引擎文件系统与异步文件读写Engine File System & Asynchronous File IO资源管理器大量依赖文件系统。引擎通常把操作系统的文件 API 包装成自己的 API:一是跨平台、屏蔽硬件差异;二是操作系统缺少引擎需要的功能(如流式加载),主机还要统一访问记忆棒、硬盘、蓝光盘、网络文件系统等介质。引擎文件系统 API 通常涵盖:文件名与路径操作、打开/关闭/读写文件、扫描目录、处理异步 I/O 请求(流式加载)。游戏开发资源管理器Resource Manager游戏由网格、材质、纹理、着色器、动画、音频、关卡布局、碰撞与物理参数等大量资源(也叫资产、媒体)构成。内存有限,所以引擎要保证每个资源在内存中只有一份(五个网格共用一张纹理时只载入一份)。每个引擎都有某种资源管理器:有的是统一集中的子系统,有的散布在多年间由不同人写的各个子系统中,但职责和要解决的问题是一样的。本页讲运行时部分:职责、文件组织与归档、文件格式、资源 GUID、资源注册表、生命周期(引用计数)和资源的内存管理;交叉引用与加载后初始化见 资源交叉引用与加载后初始化,离线工具链见 资产管线与工具链。游戏开发资源交叉引用与加载后初始化Resource Cross-References & Post-Load Initialization资源之间以任意方式相互引用(网格引用材质,材质引用纹理),整个资源数据库是一张有向依赖图。内存中的指针离开运行中的程序就失去意义,所以存盘时要把引用改写为 GUID 或文件内偏移,加载后再修正 (fix up) 回指针;含非 POD 的 C++ 对象还要用 placement new 调用构造函数;跨文件的外部引用要先加载全部相关文件。加载后通常还需要按资源类型做加载后初始化(以及释放前的拆除)。游戏开发游戏内性能与内存剖析In-Game Profiling & Memory Tracking游戏是实时系统,要保持 30 或 60 FPS,同时受目标硬件内存限制。第三方剖析工具有局限、在主机上未必可用,所以许多引擎自带游戏内剖析器(手动插桩的代码块计时、层级关系、调用次数、导出 CSV)和内存统计与泄漏检测工具。GEA 讲解它们的实现要点和常见限制。(卷 I 10.8–10.9 节)游戏开发音频引擎架构Audio Engine Architecture音频渲染软件和多数系统一样是分层的"栈":硬件 → 驱动 → 底层 API → 3D 音频引擎,功能集通常模仿录音棚的多通道调音台。每个 2D/3D 声音是一条语音 (voice),沿语音总线经过编解码、增益、预发送滤波、辅助发送分出干湿两路、混响、后发送滤波和声像,然后由主混音器合成多声道信号,再经主输出总线(预放、LFE 生成、EQ、压缩、主音量)送到扬声器。资产上分片段、音效提示 (cue)、声音库和流式声音;混音靠分组、闪避、总线预设与混音快照、实例限制和语音抢占(GEA4 卷 II 15.5)。游戏开发事件与消息传递Events & Message Passing游戏天然是事件驱动的:爆炸、玩家被敌人发现、血包被捡起都是事件,系统要通知感兴趣的对象并让它们以各自的方式处理。用虚函数 OnExplosion() 通知属于静态类型的后期绑定,迫使基类为所有可能的事件声明虚函数;GEA 主张把事件封装成对象(类型 + 参数),实现动态类型的后期绑定,从而获得单一处理函数、持久性和盲转发。GEA 依次讨论事件类型(枚举 vs 字符串/字符串 id)、参数(派生类、Variant 数组、键值对)、处理器、职责链、兴趣注册、排队与否的利弊,最后是数据驱动的事件系统和以数据通路连接端口的图形化系统(如 UE 蓝图)(GEA4 卷 II 17.8)。游戏开发游戏世界的加载与流送Loading & Streaming Game Worlds世界加载系统负责两件事:把世界块和所需资源从磁盘读进内存的文件 I/O,以及这些资源的内存分配与释放;另外还要管理游戏对象的生成与销毁。GEA 按演进顺序介绍:一次只载一个关卡的栈式加载(LSR 资源压栈底),用小块气闸 (air lock) 区域实现基本无缝的过渡,再到把所有资产切成等大块、用块池分配器按关卡加载区域流送的细粒度方案(Naughty Dog);对象生成的内存问题有"不许运行时分配"、按尺寸分池、小内存分配器和内存重定位四种对策;存档则分检查点和随处存档(GEA4 卷 II 17.4)。游戏开发游戏对象引用与查询Game Object References & Queries游戏对象需要唯一 id 来区分、查找和充当通信目标;找到对象后还要能引用它,引用的寿命可以从一次函数调用到许多分钟。GEA 比较三种实现:裸指针最快最简单,但有孤儿对象、悬垂指针、非法指针三类问题,且不能配合内存重定位;智能指针可以检测对象已删除、做引用计数,但很难写对,GEA 个人建议远离它,非用不可就用 C++11 标准库或 Boost 的成熟实现;句柄是全局句柄表的整数索引,加上唯一 id 防止槽位复用后指向错误对象,简单而安全。对象查询则不追求通用数据库,而是为最常用的查询建专门的数据结构(GEA4 卷 II 17.5)。综合游戏中的内存管理Memory Management in Games本页把 wiki 中关于内存的内容串成一条线:内存从两方面影响游戏,一是访问模式(缓存未命中远比计算昂贵),二是分配本身(通用堆分配慢且产生碎片,而主机上没有自动换页的虚拟内存兜底)。对策按生命周期组织:常驻与关卡数据用栈、每帧临时数据用单帧/双缓冲分配器、同尺寸小对象用池或小内存分配器、大小不一的动态对象用堆加碎片整理与句柄;资源按引用计数和分块加载管理,流送系统把世界切成等大的块;UE 则在 UObject 层用垃圾回收管理对象生命周期。最后,没有准确的追踪工具,以上一切都无从验证。各结论的细节与出处见所链接的概念页。图形学图形栈与渲染引擎架构Graphics Stack & Rendering Engine Architecture渲染引擎要解决的"渲染问题"是:给定由场景元素、虚拟光源和虚拟相机组成的虚拟世界,生成相机看到的二维彩色图像。GEA 把它拆成可见性问题和光照问题,按解法把引擎分为光线追踪、深度缓冲光栅化、辐射度和混合四类;光栅化引擎通常采用立即模式的两层结构:下层是图形管线,上层是负责剔除、提交几何和设置着色器参数的应用层。本页讲这套整体架构,管线内部见 渲染管线。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原