동적 상태
고친 사람 github-actions[bot]
동적 상태는 그래픽 카드에 넘기는 그리기 설정 가운데 일부를 나중에 정하게 해 줍니다. 보통 그리기 설정은 파이프라인이라는 덩어리에 미리 굳혀 둡니다. 동적 상태로 빼 둔 설정은 굳히지 않고 그리기 직전에 따로 정합니다. 그래서 화면 크기 같은 값이 바뀌어도 덩어리를 다시 만들지 않아도 됩니다.
쉽고 빠른 이해
그래픽 카드로 그릴 때는 거기서 돌 작은 프로그램(셰이더)과 설정을 한 덩어리로 묶어 미리 만들어 둡니다. 동적 상태는 그 덩어리에서 몇몇 설정을 빼 두는 표시입니다. 창 크기에 따라 달라지는 그리기 영역이 그런 설정입니다.
이게 없으면 창 크기가 바뀔 때마다 덩어리를 전부 새로 만들어야 합니다. 새로 만들 때마다 셰이더를 다시 번역하느라 시간이 걸립니다.
어떻게 도나:
- 덩어리를 만들 때 「이 설정은 나중에 정한다」고 표시합니다
- 그리기 명령을 적을 때 그 설정의 값을 따로 적습니다
- 그래픽 카드는 덩어리의 나머지 설정과 따로 적은 값을 합쳐 그립니다
대가도 있습니다. 그래픽 카드를 움직이는 소프트웨어(드라이버)는 설정을 미리 알면 셰이더를 거기 맞춰 다듬습니다. 빼 둔 설정에는 이 다듬기를 못 할 수 있습니다. 그리기 전에 값을 빠뜨리지 않고 적는 일도 그리는 쪽 코드가 맡습니다.
그래서 값이 그리기마다 바뀌는 설정은 빼 둡니다. 내내 안 바뀌는 설정은 굳혀 둡니다.
상세
이 절은 그리기 설정을 왜 미리 굳히는지, 동적 상태가 그중 무엇을 빼 내는지, 그 대가가 무엇인지를 봅니다. 창 크기를 바꿀 수 있는 그림 그리기 앱 하나를 줄곧 예로 씁니다. 앱은 그래픽 카드에 그리기를 시키는 응용 프로그램입니다.
인쇄소는 인쇄판을 새로 짜는 데 시간이 오래 걸립니다. 그래서 한 번 짠 판은 오래 씁니다. 대신 종이를 넣는 위치나 잉크 농도는 찍기 직전에 손잡이를 돌려 맞춥니다. 동적 상태가 이 손잡이입니다.
파이프라인에 굳히는 설정
이 절은 그리기 설정을 왜 파이프라인을 만들 때 미리 전부 받는지 봅니다. 미리 받아야 그 설정에 맞춰 셰이더를 다듬을 수 있기 때문입니다. 그 까닭을 보려면 그래픽 카드 안에서 무엇이 도는지부터 알아야 합니다.
GPU(Graphics Processing Unit, 그래픽 처리 장치)는 그래픽 카드의 핵심 칩입니다. 같은 계산을 동시에 아주 많이 돌립니다. 화면의 점 수백만 개의 색을 한꺼번에 정하려고 만들었습니다.
GPU 에서 도는 작은 프로그램을 셰이더라고 부릅니다. 물체의 모양을 옮기는 셰이더와 화면의 점마다 색을 정하는 셰이더가 따로 있습니다.
파이프라인은 셰이더 몇 개와 그리기 설정을 한 덩어리로 묶어 굳혀 둔 객체입니다. 색을 어떻게 섞을지, 앞에 있는 물체가 뒤를 가리게 할지 같은 설정이 함께 묶입니다. 그릴 때는 이 덩어리를 고르기만 하면 됩니다.
Vulkan은 GPU 를 다루는 API(Application Programming Interface, 응용 프로그램 인터페이스) 가운데 하나입니다. Vulkan 은 파이프라인을 만드는 순간 설정을 전부 받습니다. 동적 상태라는 이름도 Vulkan 에서 나왔습니다.
설정을 미리 전부 받는 까닭은 드라이버에 있습니다. 드라이버는 GPU 를 움직이는 소프트웨어입니다. 드라이버는 파이프라인을 만들 때 셰이더를 GPU 가 바로 돌리는 명령으로 컴파일합니다. 설정을 다 알면 그 설정에 맞춰 셰이더를 다듬을 수 있습니다.
설정이 맞는지 확인하는 일도 이때 한 번 끝냅니다. 그리는 순간마다 다시 확인하지 않으니 그리기가 가벼워집니다.
굳힌 설정이 불편해지는 경우
굳힌 설정에는 값이 자주 바뀌는 것도 섞여 있습니다. 뷰포트가 그렇습니다. 뷰포트는 그린 결과를 화면의 어느 사각형에 펼칠지 정하는 설정입니다. 창이 커지면 뷰포트도 커져야 합니다.
뷰포트를 파이프라인에 굳혀 두면 창 크기가 바뀔 때마다 파이프라인을 새로 만들어야 합니다. 그림 그리기 앱이 파이프라인을 스무 개 쓴다면 스무 개를 전부 다시 만듭니다. 그때마다 셰이더 컴파일이 다시 돕니다.
컴파일은 사람이 느낄 만큼 걸리기도 합니다. 창을 끌어 크기를 바꾸는 동안 화면 한 장(프레임)마다 컴파일이 끼면 화면이 뚝뚝 끊겨 보입니다.
조합이 불어나는 문제도 있습니다. 파이프라인은 설정이 하나만 달라도 다른 파이프라인입니다. 같은 셰이더로 카메라를 향한 면(앞면)만 그리는 설정과 양면을 다 그리는 설정이 둘 다 필요하면 파이프라인이 둘이 됩니다. 이런 설정이 셋만 겹쳐도 개수가 빠르게 늘어납니다.
동적 상태로 빼는 방법
동적 상태는 파이프라인을 만들 때 「이 설정은 굳히지 않는다」고 표시하는 방법입니다. 표시한 설정은 파이프라인에 적은 값을 쓰지 않습니다. 그리기 직전에 따로 정한 값을 씁니다.
따로 정하는 곳은 명령 버퍼입니다. 명령 버퍼는 GPU 에 시킬 일을 차례로 적어 두는 목록입니다. 앱은 이 목록에 「이 파이프라인을 쓴다」, 「이렇게 그린다」 같은 명령을 적고 모아서 GPU 로 보냅니다.
동적 상태의 값도 이 목록에 명령으로 적습니다. 파이프라인을 고르는 명령 뒤, 그리는 명령 앞에 적습니다. 그림 그리기 앱이라면 창 크기가 바뀐 뒤 뷰포트 명령에 새 크기만 적으면 됩니다. 파이프라인 스무 개는 하나도 다시 만들지 않습니다.
아래 그림은 두 때를 나눠 보입니다. 위는 파이프라인을 만들 때, 아래는 명령 버퍼에 적는 차례입니다.
flowchart TD
subgraph 파이프라인을 만들 때
S["셰이더"]
F["굳힌 설정 · 색 섞는 방식 등"]
E["동적 상태로 표시한 설정 · 값 없음"]
end
subgraph 명령 버퍼에 적을 때
B["파이프라인 고르기"]
V["뷰포트 값 적기 · 이번 창 크기"]
D["그리기 · 굳힌 설정과 적은 값을 합친다"]
B --> V --> D
end
S --> B
F --> B
E --> B
그리기 명령이 돌 때 GPU 는 파이프라인의 굳힌 설정과 명령 버퍼에 적은 값을 합칩니다. 표시만 해 두고 값을 안 적은 채 그리면 결과가 정해지지 않습니다. 동적 상태로 표시한 설정은 그리기 전에 반드시 값을 적어야 합니다.
코드로 보는 두 단계
Vulkan 에서 동적 상태를 쓰는 코드는 두 군데로 나뉩니다. 파이프라인을 만들 때 표시하는 대목과 명령 버퍼에 값을 적는 대목입니다.
예에서는 뷰포트와 함께 시저도 뺍니다. 시저는 뷰포트 안에서 그림을 그릴 사각형을 한 번 더 좁히는 설정입니다. 그 사각형 밖으로 나간 점은 버립니다.
먼저 표시하는 대목입니다.
VkDynamicState states[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
};
VkPipelineDynamicStateCreateInfo dyn = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = 2,
.pDynamicStates = states,
};
states 배열이 「뷰포트와 시저는 굳히지 않는다」는 목록입니다. 이 구조체를 파이프라인을 만드는 정보에 함께 넘깁니다.
다음은 명령 버퍼에 값을 적는 대목입니다.
vkCmdBindPipeline(cmd, bindPoint, pipe);
vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdDraw(cmd, 3, 1, 0, 0);
첫 줄이 파이프라인을 고르는 명령입니다. 둘째와 셋째 줄이 뷰포트와 시저 값을 적습니다.
마지막 줄이 삼각형 하나를 그리라는 명령입니다. 숫자 3 은 삼각형의 모퉁이인 꼭짓점 세 개를 뜻합니다. 창 크기가 바뀌면 viewport 와 scissor 에 담는 숫자만 달라집니다.
빼 둘 수 있는 설정
아무 설정이나 동적 상태로 뺄 수 있는 것은 아닙니다. 빼 둘 수 있는 설정은 목록으로 정해져 있습니다. Vulkan 이 처음 나왔을 때는 그리는 도중 자주 바뀌는 숫자 몇 개만 이 목록에 있었습니다.
표의 넷째 행은 깊이를 알아야 읽힙니다. 깊이는 화면의 점마다 적어 두는 카메라와의 거리입니다. GPU 는 새 점을 그릴 때 같은 위치에 이미 그린 점과 깊이를 견줘 더 가까운 쪽만 남깁니다. 이 견줌을 깊이 검사라고 부릅니다.
| 설정 | 무엇을 정하나 |
|---|---|
| 뷰포트 | 그린 결과를 펼칠 화면의 사각형 |
| 시저 | 그림을 남길 사각형. 밖은 버린다 |
| 선 굵기 | 선을 그릴 때 몇 픽셀 굵기로 그릴지 |
| 깊이 바이어스 | 겹친 두 면 중 한쪽의 깊이에 더하는 작은 보정. 어느 쪽을 앞으로 볼지 가른다 |
| 블렌드 상수 | 색을 섞을 때 쓰는 고정 색 하나 |
| 스텐실 기준값 | 화면 일부를 가리는 검사에 쓰는 숫자 |
마지막 행의 스텐실은 화면의 점마다 숫자를 하나씩 붙여 두는 저장 공간입니다. 그 숫자를 기준값과 견줘 그 점에 그릴지 말지를 정합니다.
나중에 이 목록이 확장으로 크게 늘었습니다. 확장은 기본 기능 위에 선택 기능을 덧붙이는 방법입니다.
늘어난 설정 하나가 컬링 방식입니다. 컬링은 카메라를 등진 면을 그리지 않고 버리는 일입니다.
VK_EXT_extended_dynamic_state 확장이 컬링 방식과 깊이 검사 켜고 끄기 같은 설정을 목록에 더했습니다. 이 설정들은 뒤에 Vulkan 의 기본 기능으로 들어왔습니다.
앞에서 든 「앞면만 그리기」와 「양면 다 그리기」가 컬링 설정입니다. 컬링을 동적 상태로 빼면 그 두 파이프라인을 하나로 합칠 수 있습니다. 조합이 불어나는 문제를 이렇게 줄입니다.
대가
동적 상태로 뺀 설정은 드라이버가 파이프라인을 만들 때 모릅니다. 그 값에 맞춰 셰이더를 다듬을 기회가 사라집니다. 얼마나 손해인지는 GPU 마다 다릅니다. 어떤 GPU 는 그 설정을 바꾸는 데 거의 비용이 들지 않습니다. 어떤 GPU 는 그리기 직전에 드라이버가 일을 더 해야 합니다.
값을 빠뜨리지 않고 적는 책임도 앱으로 넘어옵니다. 파이프라인을 바꿀 때마다 어떤 값을 다시 적어야 하는지 앱이 따라가야 합니다. 값을 빠뜨린 실수는 드라이버가 알려 주지 않습니다. 개발 중에 검증 레이어를 켜 두면 이런 실수를 잡아 줍니다. 검증 레이어는 Vulkan 호출 사이에 끼어들어 잘못된 사용을 찾아내는 도구입니다.
같은 문제를 푸는 다른 수단
파이프라인을 다시 만드는 비용을 줄이는 수단은 동적 상태 말고도 있습니다. 줄이는 방법이 서로 다릅니다.
| 수단 | 줄이는 방법 |
|---|---|
| 동적 상태 | 바뀌는 설정을 파이프라인 밖으로 빼서 파이프라인 수를 줄인다 |
| 파이프라인 캐시 | 한 번 컴파일한 결과를 남겨 두고 같은 것을 다시 만들 때 꺼낸다 |
| 파이프라인 라이브러리 | 파이프라인을 조각으로 미리 만들어 두고 이어 붙인다 |
| 셰이더 객체 | 파이프라인 없이 셰이더를 단계마다 따로 두고 설정을 전부 명령으로 정한다 |
넷은 겨루지 않고 함께 씁니다. 동적 상태로 파이프라인 수를 먼저 줄입니다. 남은 파이프라인은 캐시에 담아 둡니다. 셰이더 객체는 동적 상태를 끝까지 밀고 간 모양입니다. 굳히는 설정이 거의 없습니다.
파이프라인에 설정을 굳히는 다른 API 도 같은 생각을 씁니다. Direct3D 12·Metal·WebGPU 도 뷰포트와 시저 사각형은 파이프라인 밖에서 그리기 명령과 함께 정합니다. 반대로 OpenGL 같은 옛 API 는 설정을 하나씩 켜고 끄는 방식이라 모든 설정이 늘 동적입니다. 대신 드라이버가 그리는 순간마다 설정을 확인하고 셰이더를 고쳐야 했습니다.
쓸 때와 안 쓸 때
뷰포트와 시저는 거의 언제나 동적 상태로 뺍니다. 창 크기는 실행 중에 바뀌기 때문입니다. 그 밖의 설정은 값이 얼마나 자주 바뀌는지와 파이프라인이 몇 개로 불어나는지를 보고 고릅니다.
값이 그리기마다 바뀌거나 그 설정 하나 때문에 파이프라인이 둘로 갈린다면 빼는 쪽이 낫습니다. 값이 앱이 도는 내내 안 바뀐다면 굳혀 두는 쪽이 낫습니다. 드라이버가 그 값을 알고 다듬을 수 있기 때문입니다.
이 판단은 대개 게임 엔진과 그래픽스 라이브러리가 안쪽에서 합니다. 그 위에서 화면을 그리는 개발자는 이 구분을 직접 볼 일이 드뭅니다.
관련 항목
동적 상태가 설정을 빼 내는 파이프라인 객체
파이프라인 · 그래픽스 파이프라인 · 컴퓨트 파이프라인 · 파이프라인 상태 객체 · 파이프라인 레이아웃
동적 상태로 뺄 수 있는 그래픽스 설정
뷰포트 · 시저 테스트 · 선 굵기 · 깊이 바이어스 · 블렌딩 · 블렌드 상수 · 스텐실 버퍼 · 컬링 · 전면 판정 · 프리미티브 · 프리미티브 토폴로지 · 깊이 버퍼 · 깊이 검사
동적 상태 값을 적고 고르는 Vulkan 명령과 객체
명령 버퍼 · 파이프라인 바인딩 · 드로 콜 · 확장 · 검증 레이어
파이프라인 재생성 비용을 함께 줄이는 수단
파이프라인 캐시 · 파이프라인 라이브러리 · 셰이더 객체 · 특수화 상수 · 우버 셰이더
동적 상태가 줄이려는 지연
셰이더 컴파일 · 셰이더 컴파일 끊김 · 프레임 · 프레임 시간 · 컴파일
동적 상태와 맞세워지는 설정 방식
정적 상태 · 상태 기계 · 파이프라인 상태 · 렌더 상태
설정을 파이프라인 밖에서 정하게 하는 그래픽스 API
Vulkan · Direct3D 12 · Metal · WebGPU · OpenGL
동적 상태가 속하는 상위 분류
다른 이름: dynamic state · VkDynamicState · 동적 파이프라인 상태 · pipeline dynamic state