본질적으로 컴퓨터는 어떻게 동작을 하죠?
한줄한줄씩 순차적으로 명령어를 실행한다. 하지만 사용자 입장에서는 순차적 혹은 동시에 진행되는 여러작업을 한번에 모아서 본다.
대표적으로 게임에서의 렌더링이 그부분 해당한다. 유저에게 보여줄 화면을 그릴때, 멀리있는 산, 언덕, 나무, 유저, NPC 이런것들 보여줄떄 전부 한번에 그려서 보여준다. 하나씩 그리는 걸 눈으로 보여지게 된다면 그 과정이 눈에보이면서 게임에 몰입하기가 어렵다.
화면 장면이 빠르게 업데이트가 되어야 하고 매 프레임이 완성되면 한번에 보여줘야 한다.
또한 화면을 지우고 다시그려야 하는 문제가 있기때문에 의도치 않게 화면에 깜박임이 생기며 이를 보완해주는 방법이 더블 버퍼링이다.
들어가기 앞서 컴퓨터가 화면을 어떻게 그리는지부터 복습해보자.
컴퓨터 그래픽스 작동 원리
컴퓨터 모니터 같은 디스플레이는 한번에 한 픽셀을 그린다. 화면 왼쪽에서 오른쪽으로 한줄을 그린후에 화면 다음 줄로 내려간다.
화면 우화단에 도달하면 다시 좌상단으로 돌아가서 같은 작업을 계속 반복한다. 너무 빠르기 떄문에 (1초 60번 60FPS) 눈으로는 이것을 알아채기가 어렵다.
이 과정은 픽셀들을 좁은 호스를 통해서 화면에 전달하는 것과 유사합니다. 그럼 호스는 어떤 색을 어디에 뿌려야 하는지 알고 있을까?
대부분의 컴퓨터는 픽셀을 프레임버퍼(FrameBuffer, RenderTarget) 메모리에 할당된 픽셀들의 배열로, 한 픽셀의 색을 여러 바이트로 표현하여 정하고 있다. 해당 호스는 이 값들을 가져와서 모니터에 한픽셀씩 그려준다.
결론적으로 게임을 화면에 보여주려면 이 프레임 버퍼 안에 그림을 그려주면 된다.
앞에서 컴퓨터는 순차적으로 실행을 한다고 했는데 렌더링 코드가 실행되는 동안 다른 작업은 실행되지 않을거라고 생각한다. 보통은 맞는 얘기지만 렌더링 도중에 실행되는 작업이 있다면 프레임버퍼에서 값을 읽고 있는데 중간에 프레임버퍼 값이 바껴버리는 결과 된다. 문제가 된다.
화면에 웃는 얼굴을 하나 그린다고 해보자. 코드에서 루프를 돌면서 프레임 버퍼 픽셀값을 입력한다. 하나 몰랐던 코드가 프레임 버퍼에 값을쓰는 도중에도 비디오 드라이버에서 프레임버퍼 값을 읽는다는 점이다.
우리가 입력해놓은 픽셀 값을 비디오 드라이버가 화면에 출력하면서 웃는 얼굴이 나오기 시작하지만 아직 다 입력하지 못한 버퍼값까지 화면에 출력이 되면서 오싹한 비주얼 버그인 테어링(tearing)이 발생한다.
그래서 우리는 이를 해결하기 위한 해결책으로 더블 버퍼링 패턴이 필요하다. 이문제를 해결하려면 코드에서는 픽셀을 한번에 하나씩 그리며, 그래픽 카드에서는 전체 픽셀을 한 번에 다 읽을 수 있게 해야한다.
이전 프레임에는 얼굴 그림이 하나도 안보이다가, 다음 프레임에 전체가 한번에 보여야 한다. 더블 버퍼링 패턴이 이런문제를 어떻게 하는지 알아보자.
Scene(씬)
유저를 우리가 연출한 보는 관객이라고 상상해보겠습니다. 1장이 끝나면 2장을 시작하기 전에 무대 설치를 바꿔야 한다. 장면이 바뀔때마다 보통은 조명을 어둡게하여 안보인 상태로 바꿔준다. 화면이 끊킴을 기달려야 한다. 방법이 없을까?
공간을 좀 투자하면 가능하다. 무대를 2개를 준비 해두고 각 무대별로 조명을 각각 설치 한후에 1장은 A무대에서 2장 B무대에서 공연을 하면 해결이 가능 한 부분이다.
A화면에서 열심히 공연을 하고있고 B무대에서는 과연 기다리고 있냐? B관계자들은 C화면을 열심히 준비하여 다음 장면을 보여주기 위해 세팅하고 있다. A
B 왔다갔따 화면을 그려주는 방식을 더블 버퍼링이라고 볼 수 있다.
B 왔다갔따 화면을 그려주는 방식을 더블 버퍼링이라고 볼 수 있다.패턴
버퍼클래스는 변경이 가능한 상태인 버퍼를 캡슐화 하고 있습니다. 버퍼는 점진적으로 수정 되지만, 화면(밖)에서는 한번에 바뀌는것 처럼 보일 것이다. 이를 위해서 버퍼와, 다음에 사용할 버퍼 2개의 버퍼를 만든다.
정보를 읽을 때는 현재 버퍼에만 접근을하고, 정보를 쓸때는 현재 버퍼가 아닌 다음 버퍼에 화면 정보를 그려준다. 각각 2개를 번갈아가면서 재사용 하게 된다.
사용은 언제 해야 하는가?
•
순차적으로 변경해야하는 상태가 있을 때
•
이 상태는 변경 도중에는 접근이 가능해야 한다.
•
바깥 코드에서 작업중인 상태에 접근할 수 없어야 한다.
•
상태에 값을 쓰는 도중에도 기다리지 않고 바로 접근 할 수 있어야 한다.
교체 연산 자체가 연샹량이 존재한다.
이중 버퍼는 버퍼에 값을 다 입력했다면 다음버포 교체 되야한다. 이 교체연산자체가 많은 연산량을 활용한다.
버퍼가 2개 필요하기 떄문에 메모리를 더 많이 사용한다.
기본적으로 2개를 만들어야 하기 떄문에 메모리 사용량이 늘어단다. 공간에 대한 부담이 커진다.
예제 코드
이론은 얼추 다 이해가 됬으니 실제로 어떻게 동작하는지 코드를 살펴보도록 하자.
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];
};
C++
복사
framebuffer 클래스는 clear메서드로 전체 버퍼를 흰색으로 채워주거나, draw 메소드로 특정위치의 픽셀을 검정색으로 채워줄수 있다.
이걸 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_;
};
C++
복사
얼굴 그림이 그려진다.
게임 코드는 매 프레임마다 어떤 장면을 그려야 하는지 가르켜 준다. 먼저 버퍼를 지운 뒤에 한번에 하나씩 그리고자 하는 픽셀을 찎는다. 동시에 GPU에서 내부 버퍼에 접근한수 있또록 getBuffer()메소드를 제공한다.
별로 복잡해 보이지 않지만, 이것만으로는 문제가 생길수 있다. 아무 때나 getPixel()를 호출해 버퍼에 접근할 수 있기 떄문이다.
buffer_.draw(1, 1);
buffer_.draw(4, 1);
// <- Video driver reads pixels here!
buffer_.draw(1, 3);
buffer_.draw(2, 4);
buffer_.draw(3, 4);
buffer_.draw(4, 3);
C++
복사
이런일이 벌어지면 화면에 눈만 있고 입과 얼굴은 나오지 않은 상태로 모니터에 출력이 된다.
이문제를 해결 하기 위해서 2중버퍼로 해결보자.
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()
{
// Just switch the pointers.
Framebuffer* temp = current_;
current_ = next_;
next_ = temp;
}
Framebuffer buffers_[2];
Framebuffer* current_;
Framebuffer* next_;
};
C++
복사
이제 씬에는 버퍼가 2개가 만들어져 있다. 버퍼에 접근 할떄는 배열 대신에 next, current를 통해서 포인터 멤버변수로 접근하겠다.
이런식으로 GPU가 작업중인 버퍼에 접근하는 걸 막 을수 있다. 그리는 쪽은 무조건 next, 출력 되는쪽은 current 이기 때문에 설계구조상 서로에게 연관될수 없는 구조가 되어버렸다.
이제 이렇게 사용하면 화면깜박임이나, 테어링 이나 이런 보기 싫은 장면들이 화면에 더이상 나오지 않는다.
그래픽스 외의 활용 사례
변경 중인 상태에 접근할 수 있다는게 이중버퍼로 해결하려는 문제의 핵심이다. 원인은 보통 2가지이다. 첫번때는 다른 스레드나 인터럽트(interrupt)에서 상태에 접근하는 경우인데, 이를 그래픽스 예제에서 대충 살펴보았다. 이것만큼이나 흔한 게 어떤 상태를 변경하는 코드가, 동시에 지금 변경하려는 상태를 읽는 경우다. 특히 물리나 인공지능같이 객체가 서로 상호작용을 할때 이런 경우를 쉽게 볼수 있다. 이럴때에도 이중버퍼가 도움이된다.
인공지능
슬랩스틱 코미디 기반 게임에 들어갈 행동을 만든다고 해보자. 게임에는 무대가 준비되어 있고, 그 위에서 여러 배우들이 이런저런 몸개그를 한다.
class Actor
{
public:
Actor() : slapped_(false) {}
virtual ~Actor() {}
virtual void update() = 0;
void reset() { slapped_ = false; }
void slap() { slapped_ = true; }
bool wasSlapped() { return slapped_; }
private:
bool slapped_;
};
C++
복사
매 프레임마다 객체의 update()를 호출해 배우가 뭔가를 진행 할수 있게 해줘야 한다. 특히 유저입장에서는 모든 배우가 한번에 업데이트 되는 것처럼 보여야 한다.
그리고 또한 액터들은 서로 상호작용이 가능하다. ‘돌아가면서 서로 떄리는 것을’ 상호작용이라고 볼수 있다. update()가 호출 될 때에는 배우는 다른 배우의 객체의 slap()을 호출하고 wasSlapped()을 통해서 맞았는지 여부를 확인 할 수 있다.
class Stage
{
public:
void add(Actor* actor, int index)
{
actors_[index] = actor;
}
void update()
{
for (int i = 0; i < NUM_ACTORS; i++)
{
actors_[i]->update();
actors_[i]->reset();
}
}
private:
static const int NUM_ACTORS = 3;
Actor* actors_[NUM_ACTORS];
};
C++
복사
stage클래스는 배우를 추가 할 수 있고, 관리하는 배우 전체를 업데이트 할수 있는 update()메소드를 제공한다. 배우가 따귀를 맞았을 때 딱 한번만 반응 하기 위해서 맞은 상태를 (slapped)불변수에 저장해 두었고 업데이트가 완료된 후에 reset을 호출해 준다.
다음으로 Actor클래스를 구체화 한 Comdeian 클래스를 정의하겠다. 코미디언이 하는 일은 단순하다. 다른 배우 한 명을 보고 있다가 누구한테든 맞으면 보고 있던 배우를 떄린다.
class Comedian : public Actor
{
public:
void face(Actor* actor) { facing_ = actor; }
virtual void update()
{
if (wasSlapped()) facing_->slap();
}
private:
Actor* facing_;
};
C++
복사
어러명을 무대에 배치시켜두고 어떤일이 벌어지는지 봐보자.
Comedian* harry = new Comedian();
Comedian* baldy = new Comedian();
Comedian* chump = new Comedian();
harry->face(baldy);
baldy->face(chump);
chump->face(harry);
stage.add(harry, 0);
stage.add(baldy, 1);
stage.add(chump, 2);
C++
복사
무대는 다음과 같은 그림으로 표현이 가능하다.
Stage updates actor 0 (Harry)
Harry was slapped, so he slaps Baldy
Stage updates actor 1 (Baldy)
Baldy was slapped, so he slaps Chump
Stage updates actor 2 (Chump)
Chump was slapped, so he ... 등등
C++
복사
처음에 해리를 때린 것이 한 프레임 만에 전체 코미디언한테 전파가 된다. 이번에는 코미디언들이 바라보는 대상은 유지하되 stage배열 내에서의 위치를 바꿔 보자.
무대를 초기화하는 코드에서 나머지는 그대로 두고 무대에 배우를 추가하는 코드만 작성해주면된다.
stage.add(harry, 2);
stage.add(baldy, 1);
stage.add(chump, 0);
C++
복사
Stage updates actor 0 (Chump)
Chump was not slapped, so he does nothing
Stage updates actor 1 (Baldy)
Baldy was not slapped, so he does nothing
Stage updates actor 2 (Harry)
Harry was slapped, so he slaps Baldy
C++
복사
전혀 다른 결과가 나왔다. 문제는 명확하다. 배우 전체를 업데이트 할때 배우의 맞은 상태(slapped)를 바꾸는데, 그와 동시에 같은 값을 읽기도 하다 보니 업데이트 초반에 맞은 상태를 바꾼게 나중에 가서 영향을 미치게 된다.
결과적으로 배우가 맞았을 때 배치 순서에 따라 이번 프레임 내에서 반응 할 수도 있고 다음 프레임에서야 반응 할 수도 있다. 배우들이 동시에 행동하는 것처럼 보이고 싶었는데 이런 식으로 업데이트 순서에 따라서 결과가 다르면 안된다.
맞은 상태를 버퍼에 저장해두기
이중버퍼를 만들어서 맞은 상태만 버퍼에 저장해둔다.
class Actor
{
public:
Actor() : currentSlapped_(false) {}
virtual ~Actor() {}
virtual void update() = 0;
void swap()
{
// Swap the buffer.
currentSlapped_ = nextSlapped_;
// Clear the new "next" buffer.
nextSlapped_ = false;
}
void slap() { nextSlapped_ = true; }
bool wasSlapped() { return currentSlapped_; }
private:
bool currentSlapped_;
bool nextSlapped_;
};
C++
복사
Actor 클래스의 slapped_ 상태가 두개로 늘어 났다.앞에서 본 그래픽스 예제체럼 현재 상태(currentSlapped_)는 읽기 용도로 다음 상태(nextSlapped)는 쓰기 용도로 사용한다.
reset()메서드가 없어지고 대신 swap()메서드가 생겼다. swap()은 다음 상태를 현재 상태로 복사 한 후 다음 상태를 초기화 한다. stage클래스도 다음과 같이 약간 고쳐야 한다.
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();
}
}
C++
복사
이제 update() 메서드는 모든 배우를 먼저 업데이트한 다음에 상태를 교체한다. 결과적으로 배우객체는 자신이 맞았다는 걸 다음 프레임에서야 알 수 있다. 이제 모든 배우는 배치 순서와 상관없이 똑같이 행동한다. 유저나 빠깥 코드 입장에서는 배우가 한프레임에 동시에 일어나는 것처럼 보인다.
디자인 결정
이중버퍼는 굉장히 단순한 알고리즘이다. 사례 역시 위에서 본예제에서 벗어나지 않는다.
###
원문 전체 해설·코드·도해
위의 수업용 정리와 실습을 먼저 읽은 뒤, 아래에서 책의 원문 흐름·모든 코드·원본 도해·각주를 빠짐없이 확인할 수 있습니다. 보충 GIF는 두 버퍼의 교체 시점을 보여 줍니다.
3부 · 순서 패턴(Sequencing Patterns)
게임은 우리를 어딘가 다른 곳으로 데려가 준다는 점에서 흥미롭습니다. 우리는 몇 분 동안, 솔직히는 그보다 훨씬 오랫동안 가상 세계의 주민이 될 수 있습니다. 이런 세계를 만드는 것은 게임 프로그래머가 누리는 큰 즐거움 중 하나입니다.
게임 월드에서 중요한 축 하나가 시간입니다. 우리가 만든 가상 세계는 자신의 시간 흐름에 따라 살아 숨 쉽니다. 우리는 조물주로서 시간을 만들고, 게임 월드의 시간이 잘 흘러가도록 해야 합니다.
이번에 살펴볼 패턴들은 게임 월드의 시간을 위한 도구입니다. 게임 루프 패턴은 게임 월드 시간이 돌아가는 중심축입니다. 게임 객체는 업데이트 메서드 패턴을 통해 시간이 째깍째깍 흐르는 소리를 듣습니다. 이중 버퍼 패턴은 컴퓨터 특성상 순차적으로 작업한 것을 순간적인 스냅샷들의 파사드(facade) 뒤로 숨겨, 게임 월드가 동시에 업데이트되는 것처럼 보이게 합니다.
•
8장 · 이중 버퍼
•
9장 · 게임 루프
•
10장 · 업데이트 메서드
8.1 의도(Intent)
여러 순차 작업의 결과를 한 번에 보여줍니다.
8.2 동기(Motivation)
본질적으로 컴퓨터는 순차적으로 동작합니다. 컴퓨터의 능력은 굉장히 큰 일을 작은 단계로 쪼개 하나씩 처리할 수 있는 데 있습니다. 하지만 사용자 입장에서는 순차적으로 혹은 동시에 진행되는 여러 작업을 한 번에 모아서 봐야 할 때가 있습니다.
원주 1: 멀티코어에서 멀티스레딩을 쓰고 있다면 꼭 맞는 이야기는 아닙니다. 그래도 코어가 여러 개 있다고 해도 동시에 실행되는 명령은 몇 개 안 됩니다.
대표적인 예를 들어보겠습니다. 게임에서 렌더링은 꼭 필요합니다. 사용자에게 보여줄 게임 화면을 그릴 때는 멀리 있는 산, 구불구불한 언덕, 나무 전부를 한 번에 보여줍니다. 화면을 그리는 중간 과정이 보이면 몰입할 수 없습니다. 장면은 부드럽고 빠르게 업데이트되어야 하며, 매 프레임을 완성하면 한 번에 보여주어야 합니다.
이중 버퍼는 이런 문제를 해결합니다. 들어가기에 앞서 컴퓨터가 화면을 어떻게 그리는지부터 복습해 봅시다.
컴퓨터 그래픽스의 간단한 작동 원리
컴퓨터 모니터 같은 비디오 디스플레이는 한 번에 한 픽셀을 그립니다. 화면 왼쪽에서 오른쪽으로 한 줄을 그린 후 다음 줄로 내려갑니다. 오른쪽 아래에 도달하면 다시 왼쪽 위로 돌아가 같은 작업을 반복합니다. 이 과정이 1초에 약 60번 일어날 만큼 빠르기 때문에 눈으로는 알아챌 수 없고, 픽셀들로 이루어진 정적인 이미지를 보게 됩니다.
원주 2: 써 놓고 보니 너무 단순화하기는 했습니다. 저수준 하드웨어 개발자라면 다 아는 이야기일 테니 다음 절로 넘어가도 됩니다. 여기서는 앞으로 다룰 이중 버퍼를 이해하는 데 필요한 정도의 기반 지식만 제공하는 것이 목표입니다.
이 과정은 픽셀들을 좁은 호스를 통해 화면에 전달하는 것과 비슷합니다. 각각의 색은 하나씩, 비트별로 순서대로 호스를 통과해 화면에 뿌려집니다. 그럼 호스는 어떤 색을 어디에 뿌려야 하는지 어떻게 알까요?
대부분의 컴퓨터에서는 픽셀을 프레임버퍼(framebuffer)에서 가져옵니다. 프레임버퍼는 메모리에 할당된 픽셀들의 배열로, 한 픽셀의 색을 여러 바이트로 표현하는 RAM의 한 부분입니다. 호스는 화면에 색을 뿌릴 때 프레임버퍼에서 한 바이트씩 색깔 값을 읽어옵니다.
원주 3: 바이트 값으로 색을 어떻게 표현하는지는 픽셀 포맷(pixel format)과 색 깊이(color depth)가 정의합니다. 원서 집필 당시 대부분의 게임 콘솔은 픽셀당 32비트를 사용했습니다. 빨강·초록·파랑 채널에 8비트씩 할당하고, 나머지 8비트는 기타 여러 용도로 사용합니다.
궁극적으로 게임을 화면에 보여주려면 프레임버퍼에 값을 써 넣으면 됩니다. 온갖 심오한 그래픽 알고리즘도 결국에는 프레임버퍼에 값을 쓰기 위해 존재합니다. 다만 사소한 문제가 하나 있습니다.
앞에서 컴퓨터는 순차적이라고 했으므로, 렌더링 코드가 실행되는 동안 다른 작업은 실행되지 않을 것이라고 생각하기 쉽습니다. 보통은 맞지만 렌더링 도중에 실행되는 작업도 일부 있습니다. 그중 하나가 게임이 실행되는 동안 비디오 디스플레이에서 프레임버퍼를 반복해서 읽는 일입니다. 여기에서 문제가 발생합니다.
화면에 웃는 얼굴을 하나 그린다고 해봅시다. 코드에서는 루프를 돌면서 프레임버퍼에 픽셀 값을 입력합니다. 그런데 코드가 값을 쓰는 도중에도 비디오 드라이버는 프레임버퍼 값을 읽습니다. 우리가 입력한 픽셀을 화면에 출력하면서 웃는 얼굴이 나타나기 시작하지만, 아직 다 입력하지 못한 버퍼 값까지 출력될 수 있습니다. 그 결과 그림 일부만 나타나는 오싹한 비주얼 버그, 테어링(tearing)이 발생합니다.
원주 4 · 옮긴이: 화면 찢김 현상이라고도 합니다.
그림 8-1 · 렌더링 도중 나타나는 테어링
그림의 읽기와 쓰기 화살표를 차례로 따라가 보세요. 코드에서 픽셀 값을 입력하는 동안 비디오 드라이버도 프레임버퍼를 읽기 시작합니다(1). 결국 드라이버가 렌더러를 앞질러 아직 입력하지 않은 픽셀까지 읽습니다(2). 렌더러는 버퍼에 값을 다 그려 넣었지만(3), 드라이버는 새로 추가된 픽셀을 읽지 않습니다. 결과적으로 화면에는 그림이 반만 나타납니다(4). ‘테어링’이라는 말은 화면이 가로로 찢어진 것처럼 보인다는 데서 유래했습니다. 이것이 원주 5의 설명입니다.
이래서 이중 버퍼 패턴이 필요합니다. 코드에서는 픽셀을 한 번에 하나씩 그리되, 비디오 드라이버에서는 전체 픽셀을 한 번에 읽은 것처럼 보이게 해야 합니다. 이전 프레임에는 얼굴 그림이 하나도 보이지 않다가, 다음 프레임에는 전체가 보여야 합니다. 이중 버퍼가 이 문제를 어떻게 해결하는지 비유로 살펴보겠습니다.
1막 1장
사용자를 우리가 연출한 연극의 관객이라고 상상해 봅시다. 1장이 끝나면 2장 시작 전에 무대 설치를 바꿔야 합니다. 장면이 바뀔 때마다 무대 담당자들이 올라와 소도구를 옮긴다면 몰입감이 깨질 것입니다. 다음 장면을 준비하는 동안 조명을 어둡게 할 수도 있습니다. 실제 극장에서도 그렇게 합니다. 그래도 관객은 무언가 진행된다는 것을 알아챕니다. 끊김 없이 곧바로 장면을 전환할 방법은 없을까요?
공간을 조금 투자하면 괜찮은 해결책을 얻을 수 있습니다. 무대를 두 개 준비하고 무대마다 조명을 설치합니다. 각각 무대 A와 무대 B라고 합시다. 1장은 A에서 시작합니다. 그동안 B는 어둡게 해 놓고 담당자들이 미리 2장의 무대장치를 준비합니다. 1장이 끝나면 A의 조명을 모두 끄고 B의 조명을 켭니다. 관객은 B에서 2장이 시작되는 모습을 곧바로 볼 수 있습니다.
그동안 담당자들은 조명이 꺼진 A로 가서 1장의 장치를 철거하고 3장의 장치를 설치합니다. 2장이 끝나면 다시 A에만 조명을 비춥니다. 연극 내내 어두운 무대를 다음 장면의 준비 공간으로 활용하는 것입니다. 조명만 바꾸면 장면을 전환할 수 있으므로 기다릴 필요가 없고, 관객은 무대 담당자를 볼 일이 없습니다.
원주 6: 반투명 거울(half-silvered mirror)을 잘 배치하면 조명을 바꾸는 것만으로도 두 무대를 같은 곳에서 번갈아 보여줄 수 있습니다. 원리는 직접 찾아보세요.
다시 그래픽스로
이중 버퍼가 바로 이런 방식입니다. 거의 모든 게임의 렌더링 시스템이 내부에서 이렇게 동작합니다. 프레임버퍼를 두 개 준비합니다. 하나에는 무대 A처럼 지금 프레임에 보일 값을 두어 GPU가 원할 때 언제든 읽게 합니다.
원주 7: 모든 게임과 콘솔이 그런 것은 아닙니다. 단순한 구식 콘솔에서는 메모리가 부족해 이중 버퍼 대신 비디오 재생 빈도에 맞춰 화면을 그려야 합니다. 쉬운 일이 아닙니다.
그동안 렌더링 코드는 다른 프레임버퍼를 채웁니다. 조명이 꺼진 무대 B라고 생각하면 됩니다. 장면을 다 그렸으면 조명을 바꾸듯 버퍼를 교체하고, 비디오 하드웨어에 이제 두 번째 버퍼를 읽으라고 알려줍니다. 화면 갱신에 맞춰 버퍼가 바뀌므로 테어링 없이 전체 장면이 한 번에 나타납니다.
그 사이 이전 프레임버퍼에는 다음 프레임의 화면을 그리면 됩니다. 만세!
게임에서 활용하기: 독자가 읽는 상태를 갱신 도중에 바꾸면 반쯤 계산된 세계가 보입니다. 현재 상태를 읽는 동안 다음 상태를 다른 버퍼에 완성하고, 준비가 끝난 시점에 역할을 바꾸는 것이 핵심입니다. 버퍼 두 개를 만들었다는 사실만으로 교환 시점까지 자동으로 동기화되지는 않습니다.
8.3 패턴(The Pattern)
버퍼 클래스는 변경 가능한 상태인 버퍼를 캡슐화합니다. 버퍼 자체는 점차 수정되지만 바깥에서는 한 번에 바뀌는 것처럼 보여야 합니다. 이를 위해 현재 버퍼와 다음 버퍼, 두 개를 가집니다.
정보를 읽을 때는 항상 현재 버퍼에 접근합니다. 정보를 쓸 때는 항상 다음 버퍼에 접근합니다. 변경이 끝나면 다음 버퍼와 현재 버퍼를 교체해 새 결과를 보여줍니다. 이전의 현재 버퍼는 새로운 다음 버퍼가 되어 재사용됩니다.
보충 GIF · 읽는 버퍼를 유지하면서 다음 프레임을 작성하고, 완성 뒤 역할을 교체하는 과정
숫자 1과 2는 서로 다른 프레임입니다. 쓰기 버퍼가 일부만 바뀌는 동안에는 완성된 읽기 버퍼를 계속 보여 주고, 교체 순간에 다음 프레임 전체가 나타납니다.
8.4 언제 쓸 것인가?(When to Use It)
이중 버퍼는 언제 써야 하는지 바로 알 수 있는 패턴 중 하나입니다. 없다면 테어링처럼 시각적으로 이상해지거나 시스템이 오작동하기 때문입니다. 그래도 ‘그냥 알 수 있다’고만 하면 아쉬우니, 더 구체적인 조건을 정리해 보겠습니다.
•
순차적으로 변경해야 하는 상태가 있습니다.
•
이 상태는 변경 도중에도 접근 가능해야 합니다.
•
바깥 코드에서는 작업 중인 상태에 접근할 수 없어야 합니다.
•
상태에 값을 쓰는 도중에도 기다리지 않고 바로 접근할 수 있어야 합니다.
8.5 주의사항(Keep in Mind)
대규모 아키텍처용 패턴과 달리 이중 버퍼는 코드 구현 수준에서 적용되므로 코드 전체에 미치는 영향이 적고, 대체로 비슷한 방식으로 쓰입니다. 그래도 주의할 점이 몇 가지 있습니다.
교체 연산 자체에도 시간이 걸립니다
버퍼에 값을 다 입력했다면 버퍼를 교체해야 합니다. 교체는 원자적(atomic)이어야 합니다. 교체 중에는 두 버퍼 모두에 접근할 수 없어야 한다는 뜻입니다. 대개 포인터만 바꾸므로 충분히 빠릅니다. 하지만 버퍼에 값을 쓰는 것보다 교체하는 데 더 오래 걸린다면 이중 버퍼는 도움이 되지 않습니다.
버퍼가 두 개 필요합니다
상태를 메모리 버퍼 두 곳에 쌍으로 가지고 있어야 하므로 메모리가 더 필요합니다. 메모리가 부족한 기기에서는 큰 부담입니다. 버퍼 두 개를 만들기 어렵다면 이중 버퍼를 포기하고, 상태를 변경하는 동안 바깥에서 접근하지 못하게 할 방법을 찾아야 합니다.
8.6 예제 코드(Sample Code)
이론을 이해했으니 실제 작동을 살펴봅시다. 프레임버퍼에 픽셀을 그리는 아주 단순한 그래픽 시스템을 만들겠습니다. 콘솔과 PC에서는 이런 저수준 접근을 비디오 드라이버가 지원하지만, 직접 구현해 보면 작동 원리를 이해하는 데 도움이 됩니다. 먼저 버퍼입니다.
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];
};
C++
복사
clear()는 전체 버퍼를 흰색으로 채우고, draw()는 특정 픽셀에 검은색을 입력합니다. getPixels()를 통해 픽셀 데이터를 담은 메모리 배열에도 접근할 수 있습니다. 이 예제에서 구현하지는 않지만, 비디오 드라이버가 화면을 그리기 위해 버퍼를 읽을 때 호출하는 메서드가 getPixels()입니다.
이 버퍼를 Scene 클래스 안에 넣겠습니다. 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_;
};
C++
복사
그림 8-2 · 얼굴이려나요? Scene 클래스가 그린 예술 작품
원주 8: Scene 클래스가 그려낸 예술 작품입니다.
게임 코드는 매 프레임 어떤 장면을 그려야 하는지 알려줍니다. 먼저 버퍼를 지우고, 한 번에 하나씩 원하는 픽셀을 찍습니다. 동시에 비디오 드라이버가 내부 버퍼에 접근할 수 있도록 getBuffer()를 제공합니다.
별로 복잡해 보이지 않지만 이것만으로는 문제가 생깁니다. 비디오 드라이버가 아무 때나 버퍼에 접근해 픽셀을 읽을 수 있기 때문입니다. 책의 이 문장에서는 getPixel()이라고 적었지만, 앞에서 정의한 메서드 이름은 getPixels()입니다.
buffer_.draw(1, 1); buffer_.draw(4, 1);
// <- 이때 비디오 드라이버가 픽셀 버퍼 전체를 읽을 수도 있습니다!
buffer_.draw(1, 3); buffer_.draw(2, 4);
buffer_.draw(3, 4); buffer_.draw(4, 3);
C++
복사
이러면 눈만 있고 입은 없는 얼굴이 나옵니다. 다음 프레임에도 렌더링 도중 어딘가에서 버퍼를 읽어 버릴 수 있습니다. 결국 무섭게 깜빡거리는(flickering) 화면을 보게 됩니다. 이중 버퍼로 해결해 봅시다.
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_;
};
C++
복사
이제 Scene의 buffers_ 배열에는 버퍼 두 개가 있습니다. 버퍼에는 배열 대신 next_와 current_ 포인터 멤버 변수로 접근합니다. 렌더링할 때는 next_가 가리키는 다음 버퍼에 그리고, 비디오 드라이버는 current_를 통해 현재 버퍼에서 픽셀을 가져옵니다.
이렇게 하면 드라이버가 작업 중인 버퍼에 접근하는 것을 막을 수 있습니다. 장면을 다 그린 뒤 swap()만 호출하면 됩니다. 여기서는 두 포인터를 맞바꾸는 것이 전부입니다. 이후 드라이버가 getBuffer()를 호출하면 이전에 사용하던 버퍼가 아니라 방금 완성한 화면이 있는 버퍼를 얻습니다. 테어링이나 보기 싫은 중간 화면이 나타나지 않습니다.
그래픽스 외의 활용법
이중 버퍼가 해결하려는 핵심 문제는 변경 중인 상태에 접근할 수 있다는 것입니다. 원인은 보통 두 가지입니다. 첫째는 다른 스레드나 인터럽트(interrupt)가 상태에 접근하는 경우입니다. 그래픽스 예제에서 살펴본 상황입니다.
그만큼 흔한 둘째 경우는 어떤 상태를 변경하는 코드가 동시에 그 상태를 읽는 것입니다. 특히 물리나 인공지능처럼 객체가 서로 상호작용할 때 쉽게 볼 수 있습니다. 여기에도 이중 버퍼가 도움이 됩니다.
이웃을 읽는 시뮬레이션: 왼쪽 개체부터 갱신하면서 같은 배열에 쓰면, 오른쪽 개체는 이미 바뀐 이웃을 읽습니다. 이전 상태와 다음 상태를 나누면 갱신 순서 때문에 결과가 달라지는 문제를 줄일 수 있습니다. 추가 메모리와 역할 교환 시점의 동기화 비용은 함께 고려해야 합니다.
멍청한 인공지능
하필이면 슬랩스틱 코미디 게임의 행동 시스템을 만든다고 해봅시다. 무대 위에서 여러 배우가 이런저런 몸개그를 합니다. 먼저 배우(actor)를 위한 상위 클래스를 만들겠습니다.
class Actor {
public:
Actor() : slapped_(false) {}
virtual ~Actor() {}
virtual void update() = 0;
void reset() { slapped_ = false; }
void slap() { slapped_ = true; }
bool wasSlapped() { return slapped_; }
private:
bool slapped_;
};
C++
복사
매 프레임 배우의 update()를 호출해 무언가 진행할 수 있게 해야 합니다. 특히 사용자에게는 모든 배우가 한 번에 업데이트되는 것처럼 보여야 합니다.
원주 10: 업데이트 메서드 패턴(10장)의 예제이기도 합니다.
배우들은 서로 상호작용합니다. 돌아가면서 서로 때리는 것도 상호작용이라고 부를 수 있다면 말입니다. update()에서 다른 배우의 slap()을 호출해 때리고, wasSlapped()로 자신이 맞았는지 확인합니다.
배우들이 상호작용할 무대(stage)를 제공해 봅시다.
class Stage {
public:
void add(Actor* actor, int index) {
actors_[index] = actor;
}
void update() {
for (int i = 0; i < NUM_ACTORS; i++) {
actors_[i]->update();
actors_[i]->reset();
}
}
private:
static const int NUM_ACTORS = 3;
Actor* actors_[NUM_ACTORS];
};
C++
복사
Stage에는 배우를 추가할 수 있고, update()로 관리하는 배우 전체를 업데이트할 수 있습니다. 사용자에게는 한꺼번에 움직이는 것처럼 보여도 내부에서는 하나씩 업데이트합니다.
배우가 따귀를 맞았을 때 딱 한 번만 반응하게 하려고, 맞은 상태인 slapped_를 update() 직후 reset()으로 초기화한다는 점도 주의해서 보세요.
다음은 Actor를 상속하는 구체 클래스 Comedian입니다. 코미디언(comedian)의 일은 단순합니다. 다른 배우 한 명을 보고 있다가, 누구에게든 맞으면 보고 있던 배우를 때립니다.
class Comedian : public Actor {
public:
void face(Actor* actor) { facing_ = actor; }
virtual void update() {
if (wasSlapped()) {
facing_->slap();
}
}
private:
Actor* facing_;
};
C++
복사
코미디언 세 명을 무대에 세워 보겠습니다. 각자 다음 사람을 보고, 마지막 사람은 첫 번째 사람을 보도록 큰 원 형태로 놓습니다.
Stage stage;
Comedian* harry = new Comedian();
Comedian* baldy = new Comedian();
Comedian* chump = new Comedian();
harry->face(baldy);
baldy->face(chump);
chump->face(harry);
stage.add(harry, 0);
stage.add(baldy, 1);
stage.add(chump, 2);
C++
복사
다음 그림에서 화살표는 누가 누구를 보는지 나타내고, 숫자는 Stage::actors_ 배열의 인덱스입니다.
그림 8-3 · 게임에서 벌어지는 폭력
Harry를 때린 뒤 어떻게 되는지 보겠습니다.
harry->slap();
stage.update();
C++
복사
Stage::update()는 배열 순서대로 배우의 update()를 호출하므로 결과는 다음과 같습니다.
•
actor 0인 Harry를 업데이트합니다. Harry는 맞았으므로 Baldy를 때립니다.
•
actor 1인 Baldy를 업데이트합니다. Baldy는 맞았으므로 Chump를 때립니다.
•
actor 2인 Chump를 업데이트합니다. Chump는 맞았으므로 Harry를 때립니다.
•
Stage 업데이트가 끝납니다.
처음 Harry를 때린 것이 한 프레임 만에 전체 코미디언에게 전파됩니다. 이번에는 각자가 바라보는 대상은 유지하고 Stage 배열 안의 위치만 바꿔봅시다.
그림 8-4 · 폭력은 폭력을 낳습니다
나머지 초기화는 그대로 두고, 무대에 배우를 추가하는 코드만 바꿉니다.
stage.add(harry, 2);
stage.add(baldy, 1);
stage.add(chump, 0);
C++
복사
다시 Harry를 때려 봅시다.
•
actor 0인 Chump를 업데이트합니다. 책에는 ‘Chump가 따귀를 맞았다. Chump는 아무것도 하지 않는다’라고 적혀 있습니다.
•
actor 1인 Baldy를 업데이트합니다. 책에는 ‘Baldy가 따귀를 맞았다. Baldy는 아무것도 하지 않는다’라고 적혀 있습니다.
•
actor 2인 Harry를 업데이트합니다. Harry는 맞았으므로 Baldy를 때립니다.
•
Stage 업데이트가 끝납니다.
원문 오타를 구분해 읽기: 위 두 항목의 ‘맞았다’는 코드의 흐름과 맞지 않습니다. 새로 초기화한 무대에서 Harry만 먼저 때렸다면 Chump와 Baldy는 아직 맞지 않았기 때문에 아무것도 하지 않습니다. 원문의 두 항목을 생략하지 않고, 잘못된 표현과 실제 동작을 함께 표시했습니다.
저런, 전혀 다른 결과가 나왔습니다. 배우 전체를 업데이트하면서 맞은 상태 slapped_를 바꾸고 동시에 같은 값을 읽다 보니, 업데이트 초반의 변경이 뒤쪽 배우에게 영향을 미치는 것입니다.
원주 12: 무대를 계속 업데이트하면 때리는 동작이 매 프레임 점차 전파됩니다. 첫 프레임에 Harry가 Baldy를 때리고, 다음 프레임에 Baldy가 Chump를 때리는 식입니다.
결과적으로 배치 순서에 따라 배우가 이번 프레임 안에서 반응할 수도, 다음 프레임에야 반응할 수도 있습니다. 동시에 행동하는 것처럼 보여야 하는데 업데이트 순서 때문에 결과가 달라지면 안 됩니다.
맞은 상태를 버퍼에 저장하기
여기에서도 이중 버퍼를 쓸 수 있습니다. 이번에는 통째로 된 버퍼 객체 두 개 대신, 더 세밀하게 배우의 맞은 상태만 버퍼에 저장합니다.
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_;
};
C++
복사
Actor의 상태가 두 개로 늘었습니다. 그래픽스 예제처럼 현재 상태 currentSlapped_는 읽기에, 다음 상태 nextSlapped_는 쓰기에 사용합니다.
reset() 대신 swap()이 생겼습니다. swap()은 다음 상태를 현재 상태로 복사한 후 다음 상태를 초기화합니다. Stage도 조금 고쳐야 합니다.
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();
}
}
C++
복사
이제 모든 배우를 먼저 업데이트하고 나서 상태를 교체합니다. 배우는 자신이 맞았다는 사실을 다음 프레임에야 알 수 있습니다. 배치 순서와 상관없이 똑같이 행동하며, 사용자나 바깥 코드에는 모든 배우가 한 프레임에 동시에 업데이트되는 것처럼 보입니다.
코드를 실제로 실행할 때: 원문 생성자는 currentSlapped_만 초기화합니다. 실제 구현에서는 nextSlapped_(false)도 초기화해야 초기의 쓰레기 값을 읽지 않습니다. 첫 충격을 어느 프레임의 입력으로 삼을지도 정해야 합니다. slap()이 다음 버퍼에 쓰므로, 직후 첫 update()는 아직 그 충격을 읽지 않고 교환 뒤 프레임에서 반응합니다.
8.7 디자인 결정(Design Decisions)
이중 버퍼는 매우 단순하고, 실제 사례도 앞의 예제에서 크게 벗어나지 않습니다. 그래도 구현할 때 결정할 중요한 점이 두 가지 있습니다.
버퍼를 어떻게 교체할 것인가?
교체하는 동안 읽기와 쓰기 버퍼 모두를 사용할 수 없으므로 교체 연산은 중요합니다. 최고 성능을 얻으려면 최대한 빨리 끝내야 합니다.
버퍼 포인터나 레퍼런스를 교체한다면
그래픽스 예제의 방식이며, 이중 버퍼 그래픽스에서 가장 흔합니다.
•
빠릅니다. 버퍼가 아무리 커도 포인터 두 개만 바꾸면 됩니다. 속도와 단순성 면에서 더 좋은 방법을 찾기 어렵습니다.
•
버퍼 바깥 코드가 버퍼 메모리의 포인터를 계속 저장할 수 없습니다. 실제 데이터를 옮기는 대신, 무대 비유처럼 주기적으로 다른 버퍼를 읽으라고 알려주는 방식입니다. 바깥 코드가 버퍼 안의 데이터 포인터를 저장하면 교체 후 잘못된 데이터를 가리킬 수 있습니다. 특히 프레임버퍼가 늘 같은 메모리 위치에 있을 것으로 기대하는 비디오 드라이버에서는 문제가 됩니다. 이런 시스템에서는 이 방식의 이중 버퍼를 사용할 수 없습니다.
•
버퍼에 남은 것은 바로 이전 프레임이 아니라 두 프레임 전 데이터입니다. 데이터를 복사하지 않으면 다음처럼 번갈아 그립니다.
버퍼 A에 프레임 1을 그립니다.
버퍼 B에 프레임 2를 그립니다.
버퍼 A에 프레임 3을 그립니다.
Plain Text
복사
프레임 3을 그릴 때 A에 남아 있는 것은 프레임 2가 아니라 프레임 1의 데이터입니다. 보통 그리기 전에 버퍼를 정리하므로 문제가 없지만, 남은 데이터를 재사용한다면 두 프레임 전 것임을 고려해야 합니다.
원주 14: 이전 프레임버퍼를 사용하는 고전적인 예는 모션 블러입니다. 현재 프레임 이미지에 이전 프레임 값을 살짝 섞어 실제 카메라처럼 이미지를 뭉개는 효과입니다.
버퍼끼리 데이터를 복사한다면
사용하는 쪽이 다른 버퍼를 가리키도록 할 수 없다면 다음 버퍼를 현재 버퍼에 복사할 수밖에 없습니다. 앞의 코미디언 예제가 그렇습니다. 복사할 상태가 불리언 하나뿐이어서 포인터 교체와 속도 차이가 없었으므로 이 방법을 골랐습니다.
•
다음 버퍼에는 딱 한 프레임 전 데이터가 들어 있습니다. 이전 버퍼에서 더 최신 데이터를 얻을 수 있다는 점에서는 두 버퍼를 교체하는 방식보다 좋습니다.
•
교체 시간이 더 걸립니다. 가장 큰 단점입니다. 전체 버퍼를 복사해야 하므로 프레임버퍼처럼 크다면 매우 오래 걸릴 수 있습니다. 그동안 양쪽 버퍼를 읽고 쓸 수 없다는 제약이 큽니다.
얼마나 정밀하게 버퍼링할 것인가?
버퍼가 하나의 큰 데이터 덩어리인지, 객체 컬렉션에 분산되어 있는지도 생각해야 합니다. 그래픽스 예제는 전자이고 배우 예제는 후자입니다.
대부분은 버퍼링할 대상에 따라 답이 달라지지만 예외도 있습니다. 예를 들어 배우들이 하나의 메시지 블록에 모든 메시지를 저장하고 인덱스로 참조할 수도 있습니다.
버퍼가 한 덩어리라면
간단히 교체할 수 있습니다. 버퍼 두 개를 한 번에 맞바꾸면 됩니다. 포인터로 가리킨다면 버퍼 크기와 상관없이 포인터 대입 두 번만으로 역할을 바꿀 수 있습니다.
여러 객체가 각각 데이터를 가진다면
교체가 더 느립니다. 전체 객체 컬렉션을 순회하며 교체하라고 알려줘야 합니다.
코미디언 예제에서는 다음 맞은 상태를 정리하려고 매 프레임 버퍼 상태를 건드려야 했으므로 이 방식도 괜찮았습니다. 하지만 이전 버퍼를 건드릴 필요가 없다면, 여러 객체에 분산되어 있어도 단일 버퍼와 같은 성능을 내도록 간단히 최적화할 수 있습니다.
현재와 다음 포인터라는 개념을 객체 상대적 오프셋에 응용하는 것입니다.
class 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];
};
C++
복사
배우는 상태 배열 slapped_[2]의 current_ 인덱스로 맞은 상태를 읽습니다. 다음 상태는 나머지 원소이므로 next()로 인덱스를 계산합니다. 상태 교체는 current_만 바꾸면 됩니다. 핵심은 swap()이 정적 함수여서 한 번 호출하면 모든 배우의 상태를 교체한다는 점입니다.
확인해 보기: 버퍼 두 개만 만들면 자동으로 스레드 안전해질까요? 아닙니다. 읽는 쪽이 버퍼를 사용하는 동안 쓰는 쪽이 그 버퍼를 재사용하지 않도록 수명과 교환 시점을 동기화해야 합니다. 위 마지막 코드는 공유 인덱스로 역할을 바꾸는 핵심만 보인 예입니다. 실사용 시에는 상태 배열 초기화와 다음 상태를 지울 시점, 정적 멤버의 정의도 별도로 마련해야 합니다.
8.8 관련 자료(See Also)
이중 버퍼 패턴은 거의 모든 그래픽 API에서 찾을 수 있습니다. OpenGL의 swapBuffers(), Direct3D의 스왑 체인(swap chain), 마이크로소프트 XNA의 endDraw()에서 프레임버퍼를 교체하는 것이 그 예입니다. API 이름과 설명은 제공된 책의 집필 당시 내용을 보존했습니다.
출처: 사용자가 제공한 『게임 프로그래밍 패턴』 PDF 142~161쪽(인쇄본 141~160쪽). 3부 도입과 8장 전체이며 마지막 161쪽은 빈 페이지입니다. 본문·코드·원주 1~14와 원본 그림 8-1~8-4를 포함합니다.
















