이미지 레이아웃
고친 사람 github-actions[bot]
이미지 레이아웃은 그래픽 카드에게 이미지를 지금 어디에 쓸지 알려 줍니다. 그래픽 카드는 그 쓰임에 맞게 이미지의 메모리 배치를 바꿔 둘 수 있습니다. 프로그램은 이미지의 쓰임을 바꿀 때마다 레이아웃도 바꾸라고 명령합니다. 픽셀을 메모리에 늘어놓는 방식 일반을 뜻할 때도 있습니다. 이 문서는 그래픽 카드를 부리는 프로그래밍에서 쓰는 뜻을 다룹니다.
쉽고 빠른 이해
이미지 레이아웃은 이미지에 걸어 두는 「지금 무슨 용도」 팻말입니다. 팻말이 「그리는 중」이면 그래픽 카드는 그림을 빨리 적어 넣기 좋게 이미지를 놓아 둡니다.
이게 없으면 그래픽 카드는 이미지가 다음에 어디 쓰일지 모릅니다. 그러면 어느 용도에나 맞는 배치로 둘 수밖에 없습니다. 그 배치는 어느 용도에서도 가장 빠르지 않습니다.
어떻게 도나:
- 막 만든 이미지의 팻말은 「아직 안 정함」입니다
- 그림을 그려 넣기 전에 팻말을 「그리는 중」으로 바꾸라고 명령합니다
- 다 그린 그림을 다른 그림의 무늬로 읽으려면 「읽는 중」으로 바꾸라고 명령합니다
- 화면에 띄우기 전에는 「화면에 낼 것」으로 바꿉니다
대가는 팻말 관리를 프로그램이 맡는다는 것입니다. 바꾸는 명령을 빠뜨리면 오류 없이 화면만 깨질 수 있습니다. 팻말을 바꿀 때마다 그래픽 카드가 잠깐 기다리기도 합니다.
팻말을 프로그램이 직접 챙기는 것은 Vulkan·Direct3D 12 입니다. OpenGL·Metal·WebGPU 에서는 드라이버나 브라우저가 대신 챙겨서 프로그램에는 팻말이 안 보입니다.
상세
이 절은 먼저 같은 이미지가 왜 용도마다 다른 배치를 원하는지 봅니다. 그다음 레이아웃 몇 가지와 레이아웃을 바꾸는 명령을 Vulkan 으로 봅니다. Vulkan 은 프로그램이 그래픽 카드에 명령을 넣는 규격 가운데 하나입니다.
끝으로 이 일을 프로그램에 맡기는 그래픽 API(Application Programming Interface, 응용 프로그래밍 인터페이스)와 숨기는 API 를 나란히 놓습니다. 이어서 레이아웃이 치르게 하는 대가를 봅니다. 이름이 같은 다른 뜻은 맨 끝 소절에서 가릅니다.
여행 가방과 옷장
같은 옷도 집에 있을 때는 옷걸이에 걸어 둡니다. 꺼내 입기 편하기 때문입니다. 여행을 떠날 때는 같은 옷을 개어서 가방에 눌러 담습니다. 들고 다니기 편하기 때문입니다.
옷은 한 벌입니다. 놓는 방식만 쓰임에 따라 바뀝니다. 가방에서 옷장으로 옮길 때는 풀어서 거는 수고가 듭니다. 어디 갈지 미리 알려 주면 그 수고를 한 번에 끝낼 수 있습니다.
이미지도 이와 같습니다. 옷이 이미지입니다. 거는 방식과 개는 방식이 메모리 배치입니다. 어디 갈지 알려 주는 일이 이미지 레이아웃입니다.
이미지와 이미지를 쓰는 곳
GPU(Graphics Processing Unit, 그래픽 처리 장치)는 그래픽 카드의 계산 칩입니다. 이 문서에서 이미지는 그래픽 카드 메모리에 올려 둔 그림 데이터를 말합니다. 물체 표면에 입힐 무늬, 화면에 내보낼 결과 그림, 물체까지의 거리를 적은 그림이 모두 이미지입니다.
이미지는 네모 칸이 모인 격자입니다. 그 칸 하나를 텍셀이라고 부릅니다. 화면의 점 하나가 픽셀이듯 이미지 안의 점 하나가 텍셀입니다.
한 이미지는 한 프레임 안에서도 여러 곳에 쓰입니다. 쓰는 곳은 크게 넷입니다.
| 쓰는 곳 | 무엇을 하나 |
|---|---|
| 그리기 | 그래픽 카드가 계산한 색이나 거리 값을 이미지에 적는다 |
| 읽기 | 셰이더가 이미지를 무늬로 읽는다 |
| 복사 | 이미지의 바이트를 다른 이미지나 메모리로 옮기거나 받아 온다 |
| 화면에 내기 | 다 그린 이미지를 모니터로 보낸다 |
셰이더는 그래픽 카드에서 도는 작은 프로그램입니다. 화면의 점마다 무슨 색을 칠할지 계산합니다. 그 계산에 쓸 무늬를 이미지에서 읽습니다.
렌더 패스는 그리기 명령을 한 묶음으로 넘기는 단위입니다. 묶음을 열 때 이번에 어느 이미지에 그릴지를 함께 알립니다. 이렇게 그림이 적힐 곳으로 알린 이미지가 어태치먼트입니다.
쓰임마다 다른 메모리 배치
이미지를 메모리에 늘어놓는 가장 쉬운 방법은 한 줄씩 차례로 적는 것입니다. 첫째 줄의 텍셀을 왼쪽부터 다 적습니다. 이어서 둘째 줄을 적습니다.
그래픽 카드는 대개 이렇게 두지 않습니다. 셰이더가 무늬를 읽을 때는 가로로 이웃한 텍셀뿐 아니라 위아래 텍셀도 함께 읽기 때문입니다. 그래서 작은 네모 조각끼리 메모리에서 붙어 있게 텍셀을 섞어 늘어놓습니다. 이렇게 조각 단위로 늘어놓는 방식을 타일 배치라고 부릅니다.
그리기용 이미지는 또 다를 수 있습니다. 많은 그래픽 카드는 그림을 적을 때 메모리를 덜 오가려고 프로그램 모르게 이미지를 압축해 둡니다. 한 조각이 모두 같은 색이면 색 하나와 「전부 같다」는 표시만 적는 식입니다.
그래픽 카드 안에는 그림을 적는 부분, 무늬로 읽는 부분, 모니터로 내보내는 부분이 따로 있습니다. 이런 압축 이미지는 적는 부분이 읽고 쓰기에는 빠릅니다. 읽는 부분이나 내보내는 부분은 그 압축을 못 풀 수 있습니다. 그러면 쓰임이 바뀔 때 압축을 풀어 다시 적어야 합니다.
어떤 배치를 쓰는지는 그래픽 카드마다 다르고 프로그램에게 보이지 않습니다. 프로그램이 할 수 있는 일은 「이 이미지를 이제 무엇에 쓴다」고 알리는 것입니다. 그 알림이 이미지 레이아웃입니다.
Vulkan 의 레이아웃 몇 가지
그래픽 API 는 프로그램이 그래픽 카드에 명령을 넣는 통로입니다. 레이아웃을 프로그램에 맡기는 대표 API 가 Vulkan입니다. Vulkan 은 레이아웃마다 이름을 붙여 두었습니다.
어태치먼트에는 색 말고 다른 값이 적히기도 합니다. 거리 값은 화면의 그 점에서 물체까지 얼마나 먼지를 적은 값입니다. 가까운 물체가 먼 물체를 가리게 그릴 때 씁니다. 이 값을 모은 이미지를 깊이 버퍼라고 부릅니다.
표시 값은 화면의 점마다 붙이는 작은 정수입니다. 「이 점에는 그리지 마라」 같은 조건을 거는 데 씁니다. 이 값을 모은 이미지를 스텐실 버퍼라고 부릅니다.
이름 끝의 OPTIMAL 은 「이 쓰임에 가장 알맞게 둔다」는 뜻입니다. 코드에서는 이름 앞에 VK_IMAGE_LAYOUT_ 이 붙습니다. 자주 쓰는 것을 추리면 이렇습니다.
| 레이아웃 | 이 레이아웃에서 허용되는 쓰임 |
|---|---|
UNDEFINED |
없다. 내용을 모르는 상태다. 막 만든 이미지가 이 상태로 시작한다 |
GENERAL |
거의 모든 쓰임. 대신 가장 알맞은 배치가 아닐 수 있다 |
COLOR_ATTACHMENT_OPTIMAL |
색을 그려 넣는 어태치먼트 |
DEPTH_STENCIL_ATTACHMENT_OPTIMAL |
거리 값과 표시 값을 적는 어태치먼트 |
SHADER_READ_ONLY_OPTIMAL |
셰이더가 읽기만 한다 |
TRANSFER_SRC_OPTIMAL |
복사할 때 바이트를 내보내는 쪽 |
TRANSFER_DST_OPTIMAL |
복사할 때 바이트를 받는 쪽 |
PRESENT_SRC_KHR |
모니터로 내보낸다 |
표의 둘째 줄 GENERAL 은 어디에나 쓸 수 있는 대신 속도를 내줄 수 있는 레이아웃입니다. 나머지 OPTIMAL 들은 한 쓰임만 허용하는 대신 그 쓰임에 가장 알맞은 배치를 약속합니다.
마지막 줄 끝의 KHR 은 Vulkan 본체가 아니라 확장 기능으로 더한 이름이라는 표시입니다. 화면에 내보내는 방식은 운영체제마다 달라서 본체와 떼어 확장으로 둡니다.
화면에 내보낼 이미지를 몇 장 두고 돌려 쓰는 묶음이 스왑체인입니다. 스왑체인의 이미지는 모니터로 가기 전에 이 레이아웃이어야 합니다.
이미지 한 벌 안에서 갈리는 레이아웃
이미지 한 벌은 그림 한 장이 아닐 수 있습니다. 원래 그림을 절반씩 줄인 그림들을 함께 담기도 합니다. 이것을 밉맵이라고 부릅니다. 같은 크기의 그림을 여러 장 묶어 담기도 합니다.
이렇게 이미지 안에서 따로 고를 수 있는 한 장을 서브리소스라고 부릅니다. 레이아웃은 서브리소스마다 따로 붙습니다.
그래서 한 이미지 안에서 레이아웃이 섞일 수 있습니다. 밉맵을 만들 때가 그렇습니다. 원래 크기 그림은 복사를 내보내는 레이아웃으로 두고, 그 절반 크기 그림은 복사를 받는 레이아웃으로 둡니다.
레이아웃을 바꾸는 명령
레이아웃을 바꾸는 일을 레이아웃 전환이라고 부릅니다. 전환은 프로그램이 명령으로 요청합니다. Vulkan 에서 그 길은 둘입니다.
첫째 길은 파이프라인 배리어입니다. 파이프라인 배리어는 「앞 명령이 이 일을 끝낸 뒤에 뒤 명령이 시작하라」는 표시입니다. 여기에 이미지 하나와 「옛 레이아웃 → 새 레이아웃」을 함께 적으면 기다리는 김에 레이아웃도 바꿉니다.
기다림과 전환을 한 명령에 묶는 까닭이 있습니다. 앞의 그리기가 끝나기 전에 배치를 바꾸면 반쯤 적힌 그림이 섞입니다. 전환이 끝나기 전에 셰이더가 읽어도 같은 일이 납니다.
둘째 길은 렌더 패스입니다. 렌더 패스를 만들 때 어태치먼트마다 「시작할 때 레이아웃」과 「끝날 때 레이아웃」을 적습니다. 그러면 렌더 패스가 열리고 닫힐 때 그래픽 카드가 알아서 전환합니다.
전환은 명령을 쌓아 두는 목록인 명령 버퍼에 다른 그리기 명령과 함께 들어갑니다. 한 프레임 동안 이미지 하나가 거치는 레이아웃을 그리면 이렇습니다.
stateDiagram-v2
[*] --> UNDEFINED: 이미지를 만든다
UNDEFINED --> COLOR_ATTACHMENT_OPTIMAL: 그리기 전에
COLOR_ATTACHMENT_OPTIMAL --> SHADER_READ_ONLY_OPTIMAL: 다 그리고 읽기 전에
SHADER_READ_ONLY_OPTIMAL --> COLOR_ATTACHMENT_OPTIMAL: 다음 프레임에 다시 그리기 전에
도해의 이미지는 한 번 그려진 뒤 셰이더가 무늬로 읽습니다. 장면을 먼저 그려 두었다가 그 결과를 읽어 화면 전체를 흐리게 만드는 후처리가 이런 흐름입니다. 화살표 하나가 전환 명령 하나입니다.
옛 레이아웃을 UNDEFINED 로 적으면 뜻이 달라집니다. 「지금 들어 있는 내용은 버려도 된다」는 뜻이 됩니다. 이미지를 전부 새로 그릴 참이면 이렇게 적어서 압축을 푸는 수고를 건너뛰게 합니다.
전환 명령 코드 한 토막
위 도해의 둘째 화살표를 Vulkan 의 C 코드로 적으면 이렇습니다. 그리기를 마친 이미지를 셰이더가 읽을 수 있게 바꾸는 배리어입니다.
VkImageMemoryBarrier b = {
.sType =
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.oldLayout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.newLayout =
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.image = img, // 바꿀 이미지
.subresourceRange = range, // 고른 장
};
vkCmdPipelineBarrier(cmd, src, dst, 0,
0, NULL, 0, NULL, 1, &b);
oldLayout 과 newLayout 두 칸이 전환을 적습니다. subresourceRange 는 이미지 안에서 어느 서브리소스를 바꿀지 고릅니다.
vkCmdPipelineBarrier 가 이 배리어를 명령 버퍼에 넣습니다. 짧게 보이려고 배리어의 칸 몇 개는 코드에서 생략했습니다.
src 와 dst 는 기다림의 앞뒤를 고르는 값입니다. src 는 어떤 일이 끝나기를 기다릴지 고릅니다. dst 는 어떤 일을 그때까지 멈춰 둘지 고릅니다. 이 코드에서는 「색 그리기가 끝나면」과 「셰이더가 읽기 전에」를 넣습니다.
레이아웃을 드러내는 API 와 숨기는 API
이미지 레이아웃이 모든 그래픽 API 에서 프로그램에게 보이지는 않습니다. 여러 API 에서는 드라이버가 이미지의 쓰임을 지켜보다가 알아서 전환합니다. 드라이버는 운영체제와 그래픽 카드 사이에서 명령을 옮겨 주는 프로그램입니다.
| 그래픽 API | 레이아웃을 누가 챙기나 |
|---|---|
| Vulkan | 프로그램이 명령으로 바꾼다 |
| Direct3D 12 | 프로그램이 바꾼다. 리소스 상태라고 부르다가 새 배리어 방식에서 레이아웃이라는 이름을 쓴다 |
| OpenGL · Direct3D 11 | 드라이버가 챙긴다 |
| Metal | 대개 Metal 이 챙긴다. 프로그램에는 레이아웃이 보이지 않는다 |
| WebGPU | 브라우저가 쓰임을 추적해서 챙긴다 |
드라이버가 챙기면 프로그램은 편합니다. 대신 드라이버는 다음 명령이 무엇일지 모르고 짐작해야 합니다. 짐작이 빗나가면 필요 없는 전환이 끼거나 엉뚱한 때에 기다림이 생깁니다.
Vulkan 과 Direct3D 12 는 이 짐작을 없애려고 레이아웃을 프로그램에 넘겼습니다. 프로그램은 다음에 이미지를 어디 쓸지 알고 있으므로 필요한 때에만 전환을 넣을 수 있습니다.
레이아웃이 치르게 하는 대가
첫째 대가는 추적입니다. 전환 명령에는 옛 레이아웃을 적어야 하므로 프로그램이 이미지마다 지금 레이아웃을 기억해야 합니다. 서브리소스마다 레이아웃이 갈리면 기억할 것도 그만큼 늘어납니다.
옛 레이아웃을 틀리게 적거나 전환을 빠뜨리면 결과가 정해지지 않습니다. 오류 없이 화면이 깨질 수 있습니다. 어느 그래픽 카드에서만 깨지는 일도 있습니다. 개발 중에는 이런 실수를 잡아 주는 검증 레이어를 켜 둡니다. 검증 레이어는 API 호출이 규칙에 맞는지 검사해서 알려 주는 Vulkan 의 부가 기능입니다.
둘째 대가는 시간입니다. 전환 명령은 앞의 일이 끝나기를 기다립니다. 압축을 풀어야 하는 전환이면 이미지를 한 번 더 읽고 적습니다. 그래서 전환은 필요한 곳에만, 여러 개를 한 번에 모아서 넣습니다.
모든 곳에 GENERAL 을 쓰면 전환 대부분이 사라집니다. 그 대신 그래픽 카드가 쓰임마다 알맞은 배치를 고를 수 없어 느려질 수 있습니다.
최근 Vulkan 에는 GENERAL 을 두루 써도 느려지지 않게 하는 기능이 더해졌습니다. 이 기능을 켜도 화면에 내보낼 때는 PRESENT_SRC_KHR 로 바꿔야 합니다.
타일링과 가르는 선
이미지 레이아웃은 픽셀을 메모리에 늘어놓는 방식 일반을 부르는 말로도 쓰입니다. 앞에서 본 「한 줄씩 차례로 적는 방법」, 곧 행 우선 순서가 이쪽 이야기입니다.
한 줄이 차지하는 바이트 수는 스트라이드라고 합니다. 빨강·초록·파랑 같은 색 성분 하나하나는 채널입니다. 채널을 따로 떼어 적는지 텍셀마다 섞어 적는지도 이쪽 이야기입니다.
Vulkan 에서는 이 뜻을 이미지 타일링이라는 다른 이름이 받습니다. 타일링은 이미지를 만들 때 한 번 정하고 바꾸지 않습니다. LINEAR 로 정하면 한 줄씩 적어서 프로그램이 바이트를 직접 읽을 수 있습니다. OPTIMAL 로 정하면 그래픽 카드가 고른 배치를 써서 프로그램에게는 안 보입니다.
앞에서 본 타일 배치는 대개 이 OPTIMAL 타일링에서 쓰입니다. 이 OPTIMAL 은 타일링에 매기는 값입니다. 레이아웃 이름 끝의 OPTIMAL 과는 다른 것입니다. 레이아웃 전환이 바꾸는 것은 대개 이 타일 배치가 아니라 그 위에 얹힌 압축 같은 상태입니다.
레이아웃은 그렇게 만든 이미지가 지금 어느 쓰임에 맞춰져 있는지를 가리킵니다. 명령 하나로 몇 번이고 바뀝니다. 타일링이 이미지의 태생이라면 레이아웃은 이미지의 지금 상태입니다.
두 뜻이 만나는 레이아웃이 PREINITIALIZED 입니다. LINEAR 이미지에 프로그램이 바이트를 먼저 적어 두었을 때 쓰는 시작 레이아웃입니다. UNDEFINED 와 달리 들어 있는 내용을 버리지 않고 전환합니다.
관련 항목
이미지 레이아웃이 붙는 자원
이미지 · 텍스처 · 서브리소스 · 밉맵 · 밉맵 레벨 · 배열 레이어 · 텍셀 · 이미지 뷰 · 깊이 버퍼 · 스텐실 버퍼
이미지 레이아웃을 바꾸는 명령과 동기화 수단
레이아웃 전환 · 파이프라인 배리어 · 메모리 배리어 · 이미지 메모리 배리어 · 세마포어 · 펜스 · 서브패스 의존성 · 동기화
레이아웃에 맞춰 이미지를 쓰는 파이프라인 단계
렌더 패스 · 어태치먼트 · 셰이더 · 디스크립터 · 샘플러 · 커맨드 버퍼 · 프레임버퍼 · 렌더 타깃 · 스왑체인 · 프레젠트
레이아웃이 가리는 그래픽 카드의 메모리 배치
이미지 타일링 · 행 우선 순서 · 스트라이드 · 모턴 순서 · 델타 색 압축 · 깊이 압축 · 비디오 메모리
이미지 레이아웃을 드러내거나 숨기는 그래픽 API
Vulkan · Direct3D 12 · 리소스 상태 · 향상된 배리어 · Direct3D 11 · OpenGL · Metal · WebGPU · 디바이스 드라이버 · API
레이아웃을 잘못 다룰 때 나는 오류와 잡는 도구
렌더 패스 데이터 레이스 · 피드백 루프 · 검증 레이어 · 정의되지 않은 동작
이미지 레이아웃이 속하는 상위 분류
그래픽스 · GPU · 그래픽스 파이프라인 · 렌더링
다른 이름: image layout · VkImageLayout · 텍스처 레이아웃