이중 버퍼 (Double Buffer)

 

여러 순차 작업의 결과를 한 번에 보여준다.

 

연산 결과를 순차적으로 출력하지 않고 렌더링같이 한번에 출력해야 하는 경우 사용된다.

코드가 프레임 버퍼에 값을 쓰는 동안에 비디오 디스플레이가 프레임 버퍼의 값을 읽어버리면 이미지의 일부만 출력되는 테어링이 발생하게 된다.

 

프레임 버퍼를 두개 준비하여 입력과 출력을 서로 번갈아가며 반복하면 테어링이 발생하지 않게된다.

 

 

패턴

버퍼 클래스가 현재 버퍼와 다음 버퍼, 총 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];
};


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_;
};

 

그래픽스 외에 물리나 인공지능같이 객체가 서로 상호작용할 때 이중 버퍼를 사용하면 도움이 될 수 있다.

어떤 상태를 변경하는 코드가 일괄적으로 변경하려는 상태를 읽을 때 말이다.

 

 

디자인 결정

버퍼 교체 연산은 원자적으로 이루어져야 한다. 그에 따라서 버퍼 교체 연산 작업중에는 읽거나 쓰는 작업이 불가능해진다.

 

 

버퍼에 남아있는 데이터는 직전 프레임이 아닌 2프레임 직전의 데이터라는것을 이해해야 한다.

일반적으로는 버퍼를 정리하기 때문에 문제가 되지 않지만 모션블러같이 버퍼의 기존 데이터를 섞어서 사용하는 경우가 존재한다.

 

또한 버퍼가 하나의 큰 덩어리(그래픽스)인지, 혹은 객체의 컬렉션 안에 분산(인공지능)되어 있는지에 따라 버퍼링의 성능이 달라진다.

전자의 경우 서로 바꾸기만 하면 되기 때문에 속도가 매우 빠르다. 하지만 후자의 경우 전체 컬렉션을 순회하며 알려주어야 하기 때문에 훨씬 더 느리다. 다만 단순한 경우라면 순회하며 교환하는 대신, 정적 함수의 플래그 변수를 통해 버퍼의 인덱스만 수정하면 최적화가 가능하다.

 

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];
};

 

이런 식으로 말이다.

+ Recent posts