사전 프레임버퍼
개념

프레임버퍼

gabury1고친 사람 github-actions[bot]

프레임버퍼는 화면에 보여 줄 그림을 픽셀마다 숫자로 적어 두는 메모리입니다. 프로그램은 이 메모리에 색을 써 넣습니다. 모니터 쪽 회로는 같은 메모리를 계속 읽어 화면을 밝힙니다. 덕분에 그리는 일과 보여 주는 일이 서로 박자를 맞추지 않아도 됩니다.

쉽고 빠른 이해

프레임버퍼는 화면 한 장을 칸마다 색 번호로 적어 둔 메모리입니다. 가로 1920칸, 세로 1080칸짜리 모눈종이에 칸마다 색을 적어 둔 것과 같습니다.

이게 없으면 모니터는 1초에 수십 번 화면을 새로 밝힐 때마다 프로그램에게 "지금 무슨 색이야"를 물어야 합니다. 프로그램이 그림을 다 못 그렸으면 화면이 비어 버립니다. 중간에 적어 둔 메모리가 있으니 모니터는 거기서 읽기만 하면 됩니다.

어떻게 도나:

  1. 프로그램이 픽셀마다 색 값을 메모리에 써 넣습니다
  2. 모니터 쪽 회로가 그 메모리를 위 줄부터 차례로 읽어 화면을 밝힙니다
  3. 모니터는 1초에 수십 번 이 읽기를 되풀이합니다

모니터는 프로그램이 그리는 도중에도 읽어 갑니다. 그래서 그리다 만 화면이 가로로 찢어져 보이기도 합니다. 흔히 메모리를 두 장 둬서 막습니다. 한 장에는 그림을 그립니다. 다른 한 장은 모니터가 읽습니다. 다 그리면 둘의 역할을 맞바꿉니다.

대가는 메모리입니다. 화면이 크고 색이 정밀할수록 한 장에 드는 바이트가 늘어납니다. 두 장을 두면 그만큼 또 듭니다.

지금 화면은 거의 다 이 방식입니다. 선만 긋던 옛 화면이나 글자만 띄우는 모드는 프레임버퍼 없이 돌았습니다.

상세

이 절은 화면 한 장이 메모리에 어떻게 놓이는지부터 봅니다. 픽셀 하나에 몇 바이트가 드는지, 모니터가 그 메모리를 어떻게 읽어 가는지 차례로 따라갑니다. 끝으로 그리는 도중의 화면이 보이지 않게 막는 방법과, 그래픽 라이브러리가 이 이름을 더 넓게 쓰는 경우를 봅니다.

전광판 뒤에는 전구마다 켤 색을 적은 표가 있습니다. 전광판은 그 표를 보며 전구를 켭니다. 글자를 바꾸려는 사람은 전구를 직접 만지지 않고 표만 고칩니다. 다만 모니터는 표를 한 번 보고 끝내지 않습니다. 1초에도 수십 번 다시 읽습니다.

화면 한 장을 담는 메모리

화면은 픽셀이라는 작은 점이 바둑판처럼 늘어선 것입니다. 픽셀은 화면이 색을 따로 정할 수 있는 가장 작은 단위입니다. 가로 1920개, 세로 1080개면 픽셀이 약 200만 개입니다.

프레임버퍼는 이 픽셀들의 색을 한 줄로 이어진 메모리에 적습니다. 메모리 주소는 2차원이 아니라 1차원이기 때문입니다. 맨 위 줄의 픽셀을 왼쪽부터 적습니다. 그 뒤에 둘째 줄, 셋째 줄을 차례로 붙입니다.

flowchart TD
    subgraph 화면["화면 · 가로 3 · 세로 2"]
        R0["첫째 줄 · 픽셀 0 · 1 · 2"]
        R1["둘째 줄 · 픽셀 3 · 4 · 5"]
    end
    subgraph 메모리["프레임버퍼 · 한 줄로 이어진 메모리"]
        M["픽셀 0 · 1 · 2 · 3 · 4 · 5"]
    end
    R0 --> M
    R1 --> M

위 그림은 가로 3, 세로 2인 작은 화면입니다. 둘째 줄의 첫 픽셀은 메모리에서 네 번째 칸, 번호로는 3에 놓입니다. 줄 번호에 가로 폭을 곱하고 칸 번호를 더하면 몇 번째 픽셀인지 나옵니다.

이 계산을 코드로 옮기면 아래와 같습니다. 가로 4인 화면에서 둘째 줄의 셋째 픽셀을 찾습니다. 줄과 칸 번호는 0부터 셉니다.

Python
width = 4       # 한 줄에 픽셀 4개
x, y = 2, 1     # 셋째 칸, 둘째 줄
i = y * width + x   # 6
off = i * 4     # 24

마지막 줄의 4는 픽셀 하나가 4바이트를 차지한다는 뜻입니다. 그래서 여섯 번째 픽셀의 색은 메모리 앞에서 24바이트 떨어진 곳에서 시작합니다. 한 픽셀에 왜 4바이트가 드는지는 바로 다음 소절에서 봅니다.

픽셀 하나에 드는 바이트

모니터는 빨강·초록·파랑 세 빛을 섞어 색을 냅니다. 그래서 픽셀 하나의 색도 이 세 값으로 적습니다. 이 방식을 RGB(Red-Green-Blue, 빨강-초록-파랑)라고 부릅니다.

값 하나를 8비트로 적으면 0부터 255까지 256단계를 씁니다. 세 값이면 24비트, 3바이트입니다.

여기에 알파 값을 하나 더 붙여 RGBA(Red-Green-Blue-Alpha, 빨강-초록-파랑-알파) 네 바이트로 맞추는 경우가 흔합니다. 알파는 투명도를 뜻합니다. 그렇다고 모니터 화면이 투명해지지는 않습니다. 알파는 그림 여러 겹을 섞을 때 쓰는 값입니다. 모니터로 내보낼 때는 대개 쓰이지 않습니다.

네 바이트로 맞추면 메모리를 다루기가 단순해집니다. 픽셀 하나가 늘 4바이트 단위로 딱 떨어지기 때문입니다.

한 장에 드는 메모리는 가로 × 세로 × 픽셀당 바이트입니다. 해상도가 커지면 곱으로 불어납니다.

해상도 픽셀 수 픽셀당 4바이트일 때
1280 × 720 약 92만 약 3.5MB
1920 × 1080 약 207만 약 7.9MB
3840 × 2160 약 829만 약 31.6MB

위 표의 크기는 한 장 기준입니다. 1MB 는 1024 × 1024바이트로 셌습니다. 뒤에서 볼 두 장 방식을 쓰면 두 배가 듭니다. 채널마다 10비트나 16비트를 쓰면 색은 더 곱게 나뉘지만 메모리도 그만큼 늘어납니다.

숫자가 밝기가 되는 규칙

프레임버퍼에 적힌 것은 색이 아니라 숫자입니다. 같은 128이라도 어떤 규칙으로 읽느냐에 따라 화면의 밝기가 달라집니다. 숫자를 밝기로 바꾸는 이 규칙을 색 공간이라고 부릅니다.

모니터는 대개 sRGB(standard RGB, 표준 RGB) 색 공간으로 적힌 값을 기대합니다. 사람 눈은 어두운 쪽의 밝기 차이를 더 잘 느낍니다. sRGB 는 여기에 맞춰 0~255 눈금을 어두운 쪽에 더 촘촘히 배정합니다. 그 결과 128은 가장 밝은 값의 절반 밝기가 아니라 그보다 꽤 어둡습니다.

이 차이는 색을 섞을 때 드러납니다. 두 색의 평균을 내는 계산은 숫자가 밝기에 비례할 때 맞습니다. 이렇게 숫자가 밝기에 비례하는 표현을 선형 색 공간이라고 합니다.

그래픽 프로그램은 색을 섞는 계산을 선형 값으로 하는 경우가 많습니다. 결과를 프레임버퍼에 적을 때 sRGB 값으로 바꿉니다. 적는 순간에 이 변환을 알아서 해 주는 프레임버퍼를 sRGB 프레임버퍼라고 합니다.

모니터가 읽어 가는 방식

컴퓨터 안에는 프레임버퍼를 읽어 모니터로 신호를 보내는 회로가 따로 있습니다. 이 회로를 디스플레이 컨트롤러라고 부릅니다. 이 회로는 프로그램이 무엇을 하든 상관없이 정해진 박자로 메모리를 읽습니다.

읽는 순서는 메모리에 놓인 순서와 같습니다. 맨 위 줄 왼쪽에서 시작해 오른쪽 끝까지 가고, 다음 줄로 내려갑니다. 마지막 줄까지 읽으면 맨 위로 돌아가 다시 시작합니다. 이렇게 한 장을 훑어 화면으로 내보내는 일을 스캔아웃이라고 부릅니다.

1초에 몇 번 이 일을 되풀이하는지를 주사율이라고 부릅니다. 단위는 헤르츠(Hz)입니다. 60Hz 모니터는 1초에 60번 프레임버퍼를 처음부터 끝까지 읽습니다.

이 구조 덕분에 그리는 쪽과 보여 주는 쪽이 따로 돕니다. 프로그램이 한 장을 그리는 데 1초가 걸려도 모니터는 멈추지 않습니다. 그동안 메모리에 남아 있는 마지막 그림을 계속 보여 줄 뿐입니다.

그리는 도중의 화면이 보이는 문제

한 장짜리 프레임버퍼에는 약점이 있습니다. 프로그램이 새 장면을 위에서부터 써 내려가는 동안에도 모니터는 계속 읽습니다. 그러면 위쪽은 새 장면, 아래쪽은 옛 장면인 화면이 그대로 보입니다.

화면이 가로로 찢어진 것처럼 보이는 이 현상을 티어링이라고 부릅니다. 빠르게 움직이는 장면에서 특히 눈에 띕니다. 그리다 만 물체가 반쯤 보였다 사라지면서 깜빡이기도 합니다.

흔한 해결은 프레임버퍼를 두 장 두는 것입니다. 한 장은 모니터가 읽습니다. 다른 한 장에는 프로그램이 새 장면을 그립니다. 모니터는 그리는 중인 장을 읽지 않습니다.

두 장에는 이름이 있습니다. 모니터가 읽는 장이 프런트 버퍼, 프로그램이 그리는 장이 백 버퍼입니다. 이렇게 두 장을 쓰는 방식을 더블 버퍼링이라고 합니다.

프로그램은 백 버퍼에만 그립니다. 한 장을 다 그리면 두 버퍼의 역할을 맞바꿉니다. 이것을 스왑이라고 부릅니다. 메모리 내용을 복사하지 않고 "이제 이쪽을 읽어라"는 가리킴만 바꾸므로 순식간에 끝납니다.

sequenceDiagram
    participant 프로그램
    participant 백 as 백 버퍼
    participant 프런트 as 프런트 버퍼
    participant 모니터
    프로그램->>백: 새 장면을 처음부터 끝까지 그린다
    프런트->>모니터: 그동안 옛 장면을 계속 보여 준다
    프로그램->>백: 다 그렸다
    Note over 백,프런트: 스왑 · 두 버퍼의 이름이 맞바뀐다
    Note over 백,프런트: 새 장면이 든 쪽이 이제 프런트 버퍼다
    프런트->>모니터: 다음 스캔아웃부터 새 장면을 보여 준다

위 그림에서 모니터는 늘 완성된 장면만 받습니다. 그리다 만 백 버퍼는 모니터가 한 번도 읽지 않기 때문입니다. 스왑 뒤에는 옛 프런트 버퍼가 새 백 버퍼가 되어 다음 장면을 받습니다.

다만 스왑이 스캔아웃 도중에 일어나면 티어링이 다시 생깁니다. 위쪽은 옛 버퍼에서, 아래쪽은 새 버퍼에서 읽기 때문입니다. 그래서 모니터가 마지막 줄을 다 읽고 맨 위로 돌아가는 순간에 맞춰 스왑합니다. 이렇게 맞추는 일을 수직 동기화라고 부릅니다. 흔히 VSync(Vertical Synchronization, 수직 동기화)라고 줄여 씁니다.

수직 동기화에도 대가가 있습니다. 새 장면이 준비돼도 모니터가 맨 위로 돌아갈 때까지 기다려야 합니다. 그만큼 화면에 뜨는 때가 늦어집니다. 한 장을 그리는 데 모니터가 화면을 한 번 훑는 시간보다 오래 걸릴 수도 있습니다. 그러면 스왑이 그다음 순간으로 밀려 움직임이 끊겨 보이기도 합니다.

색 말고도 담는 버퍼들

3차원 장면을 그릴 때는 색만으로 부족합니다. 앞에 있는 물체가 뒤의 물체를 가려야 하기 때문입니다. 그래서 픽셀마다 그 점이 화면에서 얼마나 먼지도 함께 적어 둡니다. 이 값을 담는 메모리를 깊이 버퍼라고 부릅니다.

새 점을 그릴 때 깊이 버퍼의 값과 견줘서, 더 가까울 때만 색을 덮어씁니다. 이 비교를 깊이 테스트라고 부릅니다. 그리는 순서와 상관없이 가까운 물체가 앞에 보이게 됩니다.

픽셀마다 "여기는 그려도 된다, 안 된다"를 표시하는 메모리도 있습니다. 이것을 스텐실 버퍼라고 부릅니다. 거울 속 장면처럼 정해진 모양 안에만 그릴 때 씁니다.

OpenGL 같은 그래픽 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)에서는 색 버퍼 · 깊이 버퍼 · 스텐실 버퍼를 묶은 한 벌을 프레임버퍼라고 부릅니다. 여기서 색 버퍼는 지금까지 본 픽셀 색 메모리입니다.

장면을 픽셀 색으로 바꾸는 처리 단계들을 렌더링 파이프라인이라고 합니다. 이 단계들이 마지막에 내놓는 값이 이 한 벌에 쓰입니다.

화면에 안 나가는 프레임버퍼

프레임버퍼가 꼭 모니터로 이어질 필요는 없습니다. 그림을 메모리에만 그려 둘 수도 있습니다. 그 결과는 다른 그림의 재료로 씁니다. 거울에 비친 장면을 먼저 그려 두었다가 거울 표면에 붙이는 경우가 그렇습니다.

이렇게 화면 밖에서 그리는 일을 오프스크린 렌더링이라고 부릅니다. 그림을 받는 메모리는 렌더 타깃이라고 합니다.

렌더 타깃으로는 흔히 텍스처를 씁니다. 텍스처는 물체 표면에 붙이는 그림입니다. 앞의 거울 예라면 거울 속 장면을 텍스처에 그려 거울 표면에 붙입니다.

OpenGL 에서는 이런 프레임버퍼를 프로그램이 직접 만들어 씁니다. 이것을 프레임버퍼 객체라고 부릅니다.

프레임버퍼를 두지 않는 방식

모든 화면이 픽셀마다 색을 적어 두는 것은 아닙니다. 초기의 벡터 디스플레이는 픽셀 메모리 없이 선의 시작점과 끝점만 받아 전자빔으로 선을 직접 그었습니다. 메모리가 비싸던 때라 선 그림에는 이 편이 쌌습니다.

글자만 보이는 텍스트 모드도 픽셀이 아니라 칸마다 글자 번호를 적어 둡니다. 글자 모양은 회로가 따로 가진 글꼴에서 가져옵니다. 픽셀마다 색을 적는 것보다 메모리가 훨씬 적게 듭니다.

지금의 모니터는 대부분 픽셀을 칸마다 따로 켜는 방식이라 프레임버퍼를 씁니다. 메모리가 싸지면서 임의의 그림을 담을 수 있다는 장점이 메모리 비용을 이겼기 때문입니다.

관련 항목

프레임버퍼를 이루는 버퍼

색 버퍼 · 깊이 버퍼 · 스텐실 버퍼 · 알파 채널 · 누적 버퍼

프레임버퍼 한 장의 크기를 정하는 값

픽셀 · 해상도 · 색 깊이 · RGB · RGBA · 픽셀 포맷

프레임버퍼의 숫자를 밝기로 읽는 규칙

색 공간 · 선형 색 공간 · sRGB · 감마 보정 · sRGB 프레임버퍼

프레임버퍼를 모니터로 내보내는 장치와 박자

디스플레이 컨트롤러 · 스캔아웃 · 주사율 · 수직 동기화 · 모니터 · GPU

그리는 도중의 화면을 감추는 기법과 그 부작용

더블 버퍼링 · 프런트 버퍼 · 백 버퍼 · 버퍼 스왑 · 스왑 체인 · 티어링 · 입력 지연

프레임버퍼에 값을 써 넣는 처리 단계

렌더링 · 그래픽스 파이프라인 · 래스터화 · 프래그먼트 셰이더 · 깊이 테스트 · 블렌딩 · 출력 병합기

화면 밖에서 그리는 방법

오프스크린 렌더링 · 렌더 타깃 · 텍스처 · 프레임버퍼 객체

프레임버퍼를 다루는 그래픽 API

OpenGL · Vulkan · Direct3D · Metal

프레임버퍼 대신 화면을 그리던 방식

벡터 디스플레이 · 텍스트 모드 · 래스터 디스플레이

프레임버퍼가 속하는 상위 분류

그래픽스 · 비디오 메모리 · 메모리 · 래스터 그래픽스

다른 이름: framebuffer · frame buffer · 프레임 버퍼