2중 버퍼 패턴 (Double Buffer Pattern)
2중 버퍼 패턴은 게임 같은 고속 렌더링 환경에서 주로 사용되는 구현 패턴이다.
렌더링 환경
컴퓨터 모니터 같은 디스플레이는 기본적으로 픽셀 단위로 그림을 그린다.
글씨를 쓰듯이 왼쪽에서 오른쪽으로, 위에서 아래로 한번에 하나의 픽셀씩 천천히 그린다. 매우 빠르기 때문에 눈에 잘 보이지 않는 것일 뿐이다.
https://gameprogrammingpatterns.com/double-buffer.html
문제: tearing(찢어짐) 현상
게임 로직들은 당연히 "계산"과 "렌더링"을 병행해서 처리해야 한다.
뭔가 액션을 취하면 캐릭터가 이동을 하거나, 충돌 처리 같은걸 계산한 다음에 그 변한 형태대로 다시 렌더링하게끔 하는 식으로 수행이 된다.
근데 naive하게만 렌더링 로직을 작성하면, 다음과 같은 불편한 현상이 발생할 수 있다.
-
코드에서 렌더링할 픽셀 데이터들을 입력한다.
-
비디오 드라이버 등은 픽셀 데이터를 읽어서 모니터에 렌더링한다.
-
그런데, 렌더러 코드가 픽셀 데이터를 다 입력하지도 않았는데, 비디오 드라이버가 미처 완성되지 않은 화면 데이터(픽셀 데이터 집합)를 참조할 수도 있다.
-
이럴 경우에는 부분적으로만 화면이 렌더링될 수 있다. 이를 tearing이라고 부른다.
요구사항 분석
여기서 우리가 원하는 요구사항은 꽤 복잡하고 까다롭다.
-
상태 데이터를 점진적으로 쌓고 변경하는데,
-
변경 중에도 바로 읽을 수 있어야 하며
-
이 상태를 완성된 상태로만 타자(비디오 드라이버)에게 제공하고 싶은 경우.
여기엔 당연히 약간의 희생과 타협이 필요하다.
2중 버퍼 패턴
2중 버퍼 패턴은 메모리를 좀 더 쓰는걸로 위의 문제들을 해결한다.
픽셀 정보를 쓰기만 하는 write 버퍼를 하나 두고, 읽기용으로 가져다쓸 수 있는 read 버퍼를 하나 둔다.
여기서 쓰기용 버퍼는 Back buffer, 읽기용 버퍼는 Front buffer라고도 부른다.
그리고 렌더러는 쓰기 작업을 할때 우선 write 버퍼에만 쌓는다.
렌더러의 쓰기 작업이 한 턴 끝나면 write 버퍼의 작업본을 read 버퍼로 swap한다. (이 작업은 atomic해야 한다.)
https://luckyresistor.me/2019/12/07/how-to-write-custom-snowflake-patterns-1/comment-page-1/
비디오 드라이버는 read 버퍼에 저장된 완성된 사본만을 그때그때 참조해서 사용한다.
보통 포인터를 통해 접근해서 사용하고, 포인터를 스위칭해서 교체하는 방식을 취하곤 한다.
단점으로는 이 때문에 lock 매커니즘을 좀 사용해서 atomic함을 보장할 수 있어야 하고, 픽셀 데이터셋을 2중으로 관리하기 때문에 메모리 사용량이 2배로 늘어난다는 점 정도가 있다.
사용례
대부분의 상용 수준의 게임 엔진은 내부에서 이러한 기법을 적용하고 있다.
OpenGL 같은 저수준 그래픽 라이브러리에서도 이러한 기능을 간접적으로 제공한다. (OpenGL의 swapBuffers, Direct3D의 Swap chain 등)
그래픽 외에도 사용처는 무궁무진한 구현 패턴이다. 변경 중인 상태를 참조할 필요가 있는 경우에는 얼마든지 사용할 수 있다.