왜쓰나?
- 여러 순차 작업의 결과를 한 번에 보여주고 싶을 때.
- 변경 중인 상태에 접근할 수 없게 하고 싶을 때.
- 코드가 변경하려는 상태를 다시 읽는 경우.
이중 버퍼를 사용하는 가장 대표적인 예는 게임 렌더링이다.
class FrameBuffer
{
public:
void clear(); //픽셀을 모두 하얀색으로 채운다.
void draw(int x, int y); //x,y 픽셀을 검은색으로 바꾼다.
};
class Scene
{
public:
void draw();
FrameBuffer& getBuffer();
private:
FrameBuffer buffer_;
};
이처럼 FrameBuffer와 화면에 흑백 그림을 그려주는 Scene이 있다고 하자.
Scene은 화면에 곰돌이 한 마리를 그린다.
void Scene::draw()
{
buffer_.clear();
//곰돌이를 그린다.
//Part1 : 몸통 부분을 그린다
buffer_.draw(1, 5);
//계속..
//Part2 : 머리 부분을 그린다.
buffer_.draw(2, 2);
//계속..
}
여기까지는 문제가 없어 보인다. 하지만 비디오 드라이버가 몸통을 그리는 부분과 머리를 그리는 부분 사이에 호출이 된다면?
void Scene::draw()
{
buffer_.clear();
//곰돌이를 그린다.
//Part1 : 몸통 부분을 그린다
// <===!! 비디오 드라이버가 buffer_를 읽어 화면을 렌더링한다.
//Part2 : 머리 부분을 그린다.
}
화면에는 1프레임동안 머리가 없는 곰돌이가 그려지게 된다.
문제는 비디오 드라이버가 변경 중인 FrameBuffer에 접근했다는 것이다.
class Scene
{
public:
void draw();
FrameBuffer& getBuffer(); //current_를 리턴한다.
private:
void swap(); //current_와 next_를 swap
FrameBuffer buffers_[2];
FrameBuffer* current_;
FrameBuffer* next_;
};
이제 Scene은 두 개의 FrameBuffer를 가진다. swap은 단순히 포인터만 교체해주는 작업을 한다. 외부에서 프레임 버퍼에 접근할 때는 current_에만 접근할 수 있다.
void Scene::draw()
{
next_->clear();
//곰돌이를 그린다.
//Part1 : 몸통 부분을 그린다
next_->draw(1, 5);
//계속..
//Part2 : 머리 부분을 그린다.
next_->draw(2, 2);
//계속..
//다 그렸으면 swap해준다.
swap();
}
draw()에서는 next_ 버퍼에만 작업을 해준다. 외부에서는 current_에만 접근이 가능하므로 next_에는 접근이 불가능하다.
이렇게 되면 비디오 드라이버가 언제 버퍼를 읽어도 온전한 곰돌이를 볼 수 있다.
위 예제에서는 단순히 포인터를 교체하였지만 상태에 포함되는 데이터가 작은 경우 데이터를 복사하여 swap을 해줄 수 있다.
주의사항
1. 교체 연산 자체에 시간이 걸린다 : 앞서 렌더링 예의 경우 단순히 포인터를 swap하는 것이기 때문에 충분히 빠르다. 그러나 버퍼에 값을 쓰는 것 보다 교체가 더 오래 걸린다면 이중 버퍼 패턴은 무용지물이다.
2. 추가적인 메모리가 필요하다 : 추가적인 버퍼를 마련해야 하기에 그만큼 메모리가 더 필요하다.
3. 버퍼에 남은 데이터를 재사용 할 때 주의해야 한다 : 1프레임 전이 아니라 2프레임 전의 데이터를 들고 있다. (포인터 스왑 기준)
만약 이중 버퍼 패턴을 사용하지 못한다면 상태를 변경하는 동안 밖에서 접근하지 못하게 할 방법을 찾아야 한다.
참고 : 로버트 나이스트롬, 게임 프로그래밍 패턴, 한빛미디어, [143-159p]