双缓冲模式的意图
双缓冲模式,使用序列操作来模拟瞬间或者同时发生的事情
具体定义
双缓冲模式定义缓冲类封装了缓冲:一段可改变的状态。 这个缓冲被增量地修改,但我们想要外部的代码将修改视为单一的原子操作。 为了实现这点,类保存了两个缓冲的实例:下一缓冲和当前缓冲。
当信息从缓冲区中读取,它总是读取当前的缓冲区。 当信息需要写到缓存,它总是在下一缓冲区上操作。 当改变完成后,一个交换操作会立刻将当前缓冲区和下一缓冲区交换, 这样新缓冲区就是公共可见的了。旧的缓冲区成为下一个重用的缓冲区。
举个例子
接触过渲染系统的同学应该知道,图形渲染的任务就是将指定颜色的像素存储到一个缓冲区中,缓冲区中有一个二维的数组, GPU负责读取这个数组,根据数组中的颜色信息,将图像绘制在屏幕上。我们可以这样定义这个缓冲区:
class Framebuffer
{
public:
Framebuffer() { clear(); }
void clear()
{
for (int i = 0; i < WIDTH * HEIGHT; i++)
{
pixels_[i] = WHITE;
}
}
void draw(int x, int y)
{
pixels_[(WIDTH * y) + x] = BLACK;
}
const char* getPixels()
{
return pixels_;
}
private:
static const int WIDTH = 160;
static const int HEIGHT = 120;
char pixels_[WIDTH * HEIGHT];
};
其中pixels就是我们说的二维数组,GPU会频繁调用函数,将缓冲区中的数据(通常是颜色信息)输送到屏幕上。
我们将整个缓冲区封装在Scene类中。渲染某物需要做的是在这块缓冲区上调用一系列draw()。
class Scene
{
public:
void draw()
{
buffer_.clear();
buffer_.draw(1, 1);
buffer_.draw(4, 1);
buffer_.draw(1, 3);
buffer_.draw(2, 4);
buffer_.draw(3, 4);
buffer_.draw(4, 3);
}
Framebuffer& getBuffer() { return buffer_; }
private:
Framebuffer buffer_;
};
当draw()绘制完毕后,GPU就可以调用getBuffer()将数组中的颜色信息读取出来,然后在屏幕上绘制对应的信息了。
问题在于,我们的GPU会在固定的渲染帧频率期间调用 getBuffer()方法,读取pixel中的数据。如果我们绘制的任务比较重,那么就有可能出现draw()还没结束,GPU就迫不及待读取了,这就会造成画面错误。
当上面的情况发生时,用户就会看到脸的眼睛,但是这一帧中嘴却消失了。 下一帧,又可能在某些别的地方发生冲突。最终结果是糟糕的闪烁图形。
我们会用双缓冲模式修复这点:
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_;
};
现在scene中有两个缓冲区,next_是我们正在绘制的数组。Current_是已经准备就绪,CPU可以访问到的数组。每次绘制完毕,我们只要交换一下两个数组的指针就好了。在需要时,GPU读取current_获取数据绘制图像。因为GPU不会接触到正在施工的数据,所以可以保证每次都是正确的图像。
一些要点
- Swap本身也需要时间,我们必须要保证swap的操作是原子的,也就是说,在这个过程中,没有人可以接触到这两个缓冲区的任何数据,这样才能保证双缓冲模式正确运行。
双缓冲需要保存两个缓冲区,这可能是个问题,你得看看自己的内存够不够用。 - 原文里还用扇耳光游戏说明了双缓冲模式在别的地方的运用,这里没有做相关笔记。大体上来说,双缓冲模式保证了这个游戏所有人对于巴掌的反应都在下一帧才生效,这让它们在同一帧中更新的顺序不该对结果有影响。
使用场合
- 我们需要维护一些被增量修改的状态。
- 在修改到一半的时候,状态可能会被外部请求。
- 我们想要防止请求状态的外部代码知道内部的工作方式。
- 我们想要随时可以读取状态,而且不想等着修改完成。