问题引入
尽管计算机的处理能力相比过去有了极大的进步。但对于实时渲染的游戏程序,仍旧不能在一瞬间将同屏的所有物件全部加载出来。即使可以,用户的硬件条件参差不齐,若运行在性能较低的机器,用户将看到破碎断续加载的画面。
从更底层形象地看断续加载的原因,就要了解画面是如何绘制的。计算机维护着一个帧缓冲区,游戏想要显示画面,就需要将像素颜色信息填写到帧缓冲区中。而显示设备就从帧缓冲区中读取像素信息并会知道屏幕上。这时候就会出现一个同步问题——当显示设备从帧缓冲区读取到计算机未填充的信息时,就会照成原本想要绘制的画面像素缺失,导致画面撕裂,从而表现为断续加载。
如何解决计算填充像素能力跟不上显示器读取像素的频率的问题,双缓冲给出了一个比较好的解决方案。
双缓冲方案
两个人赛跑,已知其中一个速度快,一个速度慢。那么如何让那个速度跑的慢的长时间能够保证领先于速度快的人?很简单,就是让速度慢的人提前跑很长一段时间。双缓冲的原理就是,计算机维护两个帧缓冲区,当计算机填充好一个帧缓冲区后,就交给显示设备进行读取,再显示设备读取此帧缓冲区的过程中,计算机已经开始填充另一个缓冲区。这样的错开使得每一次显示设备都能获取到数据完整的帧缓冲区,也不会出现断续加载、画面撕裂的情况。
双缓冲模式抽象出来,可以总结出它的使用范围:
- 我们需要维护一些被逐步改变的、数据量较大的状态量
- 这两个状态可能存在读写同步的问题(可能存在同时进行读和写操作)
- 我们希望避免访问状态的代码能看到具体的工作过程
- 我们希望读和写操作不要相互等待,尤其是读操作等待写操作完成
双缓冲注意事项
- 交换本身需要时间。在图形渲染API中,交换时间就是两个指针交换值的时间。注意这个时间,当交换时间大于等待时间,双缓冲就没有了意义。
- 双缓冲增加了内存的使用。相比于单缓冲,双缓冲多维护了一个缓冲区。这就是用空间换时间。
示例代码
//帧缓冲区
class FrameBuffer
{
public:
static const int screen_width=1920;
static const int screen_heigh=1080;
void SetPixel(int x,int y,Color c)
{
if(x<screen_width&&y<screen_heigh)
pixels[x][y]=c;
}
Color GetPixel(int x,int y)
{
if(x<screen_width&&y<screen_heigh)
return pixels[x][y];
else
return Color.Default;
}
void Clear()
{
for(int i=0;i<screen_width;i++)
for(int j=0;j<screen_heigh;j++)
pixels[i][j]=Color.White;
}
Color* GetPixels()
{
return pixels;
}
private:
Color pixels[screen_width][screen_heigh];
}
//渲染类
class Render
{
public:
void Draw()
{
FrameBuffer buffer=frames[drawCount%(FRAME_COUNT-1)];
//填充数据
buffer.SetPixel(0,0,Color.red);
//...
Swap();
}
void Swap()
{
currentFrame=drawCount%(FRAME_COUNT-1);
}
FrameBuffer GetDisplayBuffer()
{
return frames[currentFrame];
}
private:
static const int FRAME_COUNT=2;
FrameBuffer frames[FRAME_COUNT];
int currentFrame=0;
int drawCount=0;
}
//显示设备类
class DisplayAdaptor
{
public:
void Display()
{
FrameBuffer buffer=GetDisplayBuffer();
//...
}
}