中EN
概念页

双缓冲

Double Buffer

更新于 2026-10-11 约 8 分钟设计模式双缓冲帧缓冲撕裂并发GPP

双缓冲 (Double Buffer) 让一系列按顺序执行的修改,在外界看来像是瞬间、同时完成的:类内部保留“当前缓冲”和“下一缓冲”两份状态,读取总是读当前缓冲,写入总是写下一缓冲,改完后用一次原子交换让新状态整体生效。图形里的前后缓冲是最经典的例子;同样的思路也能让一帧内依次更新的游戏对象互相看到的都是上一帧的一致状态。

意图

让一系列顺序操作看起来是瞬间或同时完成的。(Game Programming Patterns — double-buffer)

机制

被缓冲的类封装一块会被修改的状态(缓冲区)。这块状态是逐步修改的,但希望外部代码把整次修改看成一个原子变化。做法是保留两份:下一缓冲和当前缓冲。读取永远读当前缓冲,写入永远写下一缓冲;修改完成后执行交换,新缓冲立即对外可见,旧的当前缓冲则作为下一次的写入缓冲复用。(Game Programming Patterns — double-buffer)

为什么需要:显示器逐像素读取帧缓冲的同时,程序也在逐像素写入。读取速度超过写入时,就会读到还没画好的像素,画面上出现半新半旧的撕裂 (tearing)。程序是一个像素一个像素地画,但必须让显示驱动一次看到整幅画面。书中的类比是两座舞台:观众看 A 台演第一幕时,B 台在黑暗中布置第二幕,幕间只需切换灯光;随后又在暗下来的 A 台布置第三幕。作者也提到,并非所有游戏都这么做,内存有限的老式简单主机会小心地让绘制与显示刷新同步。(Game Programming Patterns — double-buffer)

不只用于图形:状态在修改过程中被访问,有两种常见原因。一是状态被另一个线程或中断直接访问(图形例子);二是做修改的代码本身也在读它正在修改的状态,这在实体互相作用的物理和 AI 中很常见。(Game Programming Patterns — double-buffer)

下图是一帧内的读写分工与交换;交换期间两个缓冲都不能被访问。

sequenceDiagram
  participant W as 写入方(渲染/更新代码)
  participant N as 下一缓冲
  participant C as 当前缓冲
  participant R as 读取方(显示/其他对象)
  W->>N: 逐步写入本帧内容
  R->>C: 读取上一帧的完整内容
  Note over N,C: 写完后 swap():交换指针或拷贝
  R->>C: 之后读到的是新的完整帧

图中读取方从不接触写到一半的缓冲;代价是多一份内存,外加一次必须原子完成的交换。

何时使用

同时满足以下条件时使用(Game Programming Patterns — double-buffer):

  • 有一块状态被逐步修改;
  • 这块状态可能在修改过程中被访问;
  • 不希望访问方看到修改到一半的结果;
  • 希望读取时不必等待写入完成。

书中第二个例子是几个互相“扇耳光”的喜剧演员:如果在同一帧里按顺序更新,被打者是否在本帧就做出反应,取决于它在数组里排在打人者前面还是后面。给“被打”状态加双缓冲后,每个人在本帧读到的都是上一帧的结果,更新顺序不再影响行为。代价是每个演员都要到被打的下一帧才看到这一耳光。这里交换采用拷贝:swap() 把“下一”状态拷进“当前”再清空“下一”,Stage::update() 改为先更新所有演员、再统一交换。(Game Programming Patterns — double-buffer)

取舍与坑

设计决策(Game Programming Patterns — double-buffer)

决策选项特点
如何交换交换指针或引用快,与缓冲大小无关;外部代码不能长期持有指向缓冲内部的指针(显卡驱动要求帧缓冲地址固定时就不能用);缓冲里残留的是两帧前的数据(若要复用旧数据,例如把上一帧混合进来模拟运动模糊,就要注意这一点)
在两个缓冲之间拷贝数据喜剧演员例子用的就是这种,因为一个布尔值拷贝起来不比指针慢;下一缓冲里的数据只旧一帧;缓冲很大时交换很慢,期间两边都不可访问
缓冲粒度整块缓冲交换简单,一次即可
分散在许多对象里交换要遍历所有对象;可以把“当前/下一”变成每个对象内数组的下标,用一个静态的 current_ 一次切换全部对象

代码草图(C++)

按书中示例整理:图形例子(交换指针)、喜剧演员例子(拷贝交换),以及“分散缓冲 + 静态下标”的变体。

class Scene {
public:
  Scene() : current_(&buffers_[0]), next_(&buffers_[1]) {}

  void draw() {
    next_->clear();
    next_->draw(1, 1);
    // ...
    next_->draw(4, 3);
    swap();
  }

  Framebuffer& getBuffer() { return *current_; }   // 显示驱动只读当前缓冲

private:
  void swap() {                                    // 只交换指针
    Framebuffer* temp = current_;
    current_ = next_;
    next_ = temp;
  }

  Framebuffer  buffers_[2];
  Framebuffer* current_;
  Framebuffer* next_;
};

// 喜剧演员:细粒度缓冲“被打”状态,交换时拷贝
class Actor {
public:
  Actor() : currentSlapped_(false) {}
  virtual ~Actor() {}
  virtual void update() = 0;

  void swap() {
    currentSlapped_ = nextSlapped_;   // 交换缓冲
    nextSlapped_ = false;             // 清空新的“下一”缓冲
  }
  void slap()       { nextSlapped_ = true; }
  bool wasSlapped() { return currentSlapped_; }

private:
  bool currentSlapped_;
  bool nextSlapped_;
};

void Stage::update() {
  for (int i = 0; i < NUM_ACTORS; i++) actors_[i]->update();
  for (int i = 0; i < NUM_ACTORS; i++) actors_[i]->swap();
}

// 变体:每个对象存两份,用静态下标一次切换所有对象
class Actor2 {   // 书中仍名为 Actor
public:
  static void init() { current_ = 0; }
  static void swap() { current_ = next(); }
  void slap()        { slapped_[next()] = true; }
  bool wasSlapped()  { return slapped_[current_]; }
private:
  static int current_;
  static int next()  { return 1 - current_; }
  bool slapped_[2];
};

渲染与 UE 中的对应

  • 图形 API 的前后缓冲 → GPU 内存、缓冲压缩与多重缓冲:前缓冲显示已完成的图像,后缓冲用于绘制,通常在 vsync 时交换指针;不等 vsync 立即交换可以拉高帧率,但会撕裂。Nystrom 也指出 OpenGL 的 swapBuffers()、Direct3D 的交换链都是这个模式。(Game Programming Patterns — double-buffer)
  • 游戏线程与渲染线程的状态镜像 → UE5 渲染架构:UE 的渲染线程用“场景代理”镜像游戏线程的组件状态,默认流水线“落后一帧”(游戏线程处理第 N+1 帧时,渲染线程处理第 N 帧)。游戏线程改了组件属性必须调用 MarkRenderStateDirty() 才会同步,渲染线程不得直接读 UPrimitiveComponent。这与双缓冲的思想相近:读方只看一份已经确定的状态,写方在另一份上修改。但它是线程间的状态复制,不是严格的两个缓冲加一次交换,这里只作类比。
  1. Game Programming Patterns — double-buffer
  2. Game Programming Patterns — double-buffer(全文版)
  3. Robert Nystrom《Game Programming Patterns》(2014) double-buffer 章,gameprogrammingpatterns.com/double-buffer.html

相关页面

游戏开发游戏循环与更新方法Game Loop & Update Method游戏循环 (Game Loop) 让游戏时间的推进与玩家输入和处理器速度脱钩:每一轮非阻塞地处理输入、更新状态、渲染,并跟踪真实时间来控制节奏;最常推荐的做法是“固定更新步长、可变渲染”。更新方法 (Update Method) 是它的搭档:世界维护一组对象,每个对象实现一个模拟“一帧”行为的 update(),每帧依次调用。两者和组件模式一起常被视为游戏引擎的核心。图形学GPU 内存、缓冲压缩与多重缓冲GPU Memory, Buffer Compression & Multiple Buffering带宽是稀缺资源。GPU 通过独立显存或统一内存架构 (UMA)、多级缓存与按瓦片存储提高局部性,并用基于瓦片表的无损压缩和快速清除减少渲染目标的带宽(注意:无损压缩只省带宽,不省内存)。颜色缓冲有高彩、真彩、深色三种模式;显示控制器以固定刷新率或 FreeSync/G-sync 自适应同步把颜色缓冲送到显示器;单/双/三缓冲在撕裂、等待与延迟之间取舍。游戏开发 · UE5UE5 渲染架构UE5 Rendering ArchitectureUE5 的渲染器是独立的 Renderer 模块,在渲染线程 (Rendering Thread) 上以「场景代理」镜像游戏线程的组件状态,每帧按固定顺序执行深度预通道、G-buffer 基础通道、光照、雾、半透明和后处理。现代渲染代码用渲染依赖图 (Render Dependency Graph, RDG) 声明通道与资源,由 RDG 自动排屏障、复用瞬态内存、并行录制;最终命令经 RHI 线程翻译成 D3D12/Vulkan 等图形 API 调用。默认走延迟着色 (Deferred Shading),也可切换前向着色 (Forward Shading)。综合游戏编程模式选型:解耦与性能Choosing Game Programming Patterns本页把《Game Programming Patterns》里的 19 个模式放到同一张取舍图上:大多数模式要么为了解耦(降低改代码时需要装进脑子里的知识量),要么为了性能(利用对数据和硬件的具体假设),而这两个目标经常互相拉扯。下面先讲全书的定位和 Nystrom 的架构观,再给出按问题选模式的路线和与 UE 的对照。游戏开发游戏对象的实时更新Updating Game Objects in Real Time游戏对象更新就是根据上一时刻状态 Si(t − Δt) 求出当前状态 Si(t)。"每帧对每个对象调一次虚函数 Update(),并在里面顺带更新它的动画、物理、渲染组件"的简单做法在商业引擎里行不通:底层子系统需要批量更新才能高效,而对象之间、子系统之间又有依赖顺序。GEA 介绍的对策依次是:子系统由游戏循环直接批量更新、对象只操纵子系统中的"代理状态";在帧内多个阶段挂回调;按依赖森林把对象分桶,逐桶运行完整的分阶段更新(Naughty Dog 的做法);并用状态缓存和时间戳对付更新过程中的状态不一致和一帧延迟(GEA4 卷 II 17.6)。游戏策划复杂系统与涌现Complex Systems & EmergenceAdams & Dormans 第 3 章把玩法看作游戏这一复杂系统 (Complex System) 的涌现属性:涌现处于有序与混沌之间,只有系统运转起来才会出现,所以只能靠原型与测试。借助复杂性科学,作者提炼出促成涌现的三个结构特征——活跃且相互连接的部件、反馈回路、不同尺度上的行为——并引入 Fromm 的涌现分类,作为 Machinations 框架的出发点。游戏策划Machinations 图示语言Machinations DiagramsMachinations 是 Joris Dormans 设计的可视化语言与在线模拟工具,用于创建、记录、模拟和测试游戏的内部经济。其理论前提是:玩法最终由有形、无形与抽象资源在游戏系统中的流动决定。图由存放资源的节点 (Nodes)、搬运资源的实线资源连接 (Resource Connections)、以及用当前状态去修改别处的虚线状态连接 (State Connections) 组成;语法足够严格,可被计算机按时间步执行,接近一种专门表示游戏机制的可视化编程语言。本页按第 5 章整理全部基本元素与《吃豆人》建模示例,并给出本 wiki 用 Mermaid 改画 Machinations 图游戏开发游戏时间线与时钟Game Timelines & Clocks游戏引擎要同时处理多种「时间」:真实时间、游戏时间、动画片段的局部时间、CPU 周期等。GEA 用抽象时间线 (abstract timeline) 统一看待它们,并讨论帧时间 \Delta t 的测量方法、高精度计时器、时间单位与时钟变量类型的取舍,以及断点恢复后的异常 \Delta t 处理。(卷 I 第 8 章 引言、8.4–8.5 节)

输入关键词开始搜索

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