사전 디스크립터 세트 레이아웃
개념

디스크립터 세트 레이아웃

gabury1고친 사람 github-actions[bot]

디스크립터 세트 레이아웃은 그래픽 카드에서 도는 프로그램에게 건넬 데이터 묶음이 어떤 칸으로 나뉘는지를 미리 정해 둡니다. 칸마다 번호와 들어올 데이터의 종류를 적어 둡니다. 데이터 묶음은 이 틀에 맞춰 찍어 냅니다. 그래픽 카드 쪽은 이 틀을 보고 묶음을 담을 메모리와 데이터를 읽는 길을 미리 준비합니다.

쉽고 빠른 이해

디스크립터 세트 레이아웃은 데이터 묶음의 칸 배치를 먼저 정해 둡니다. 의자의 재질 묶음이라면 「0번 칸은 나뭇결 그림, 1번 칸은 그림을 읽는 규칙, 2번 칸은 광택 값」이라고 적습니다. 같은 틀로 의자 묶음도 책상 묶음도 찍어 냅니다.

그래픽 카드 쪽은 칸 배치를 미리 알아야 일을 시작할 수 있습니다. 묶음 하나에 메모리가 얼마나 드는지, 그림을 그리는 프로그램이 몇 번 칸을 어떻게 읽을지가 전부 칸 배치에서 나옵니다. 틀을 먼저 받아 두면 이 계산을 그릴 때마다 하지 않습니다.

어떻게 도나:

  1. 칸마다 번호, 종류, 개수, 그 칸을 읽을 프로그램을 적어 틀을 만듭니다
  2. 그 틀로 묶음을 찍어 냅니다. 어떤 프로그램으로 어떻게 그릴지 미리 묶어 둔 그리는 설정을 만들 때도 같은 틀을 넘깁니다
  3. 그릴 때는 같은 틀로 찍은 묶음만 그 번호에 끼울 수 있습니다

대가는 틀을 고칠 수 없다는 것입니다. 칸을 하나 더하려면 틀을 새로 만듭니다. 그 틀로 찍은 묶음과 그리는 설정도 새로 만듭니다.

틀을 손으로 만들 일은 드뭅니다. 게임 엔진이 안에서 대신 만듭니다. 그래픽 카드를 직접 다루는 라이브러리(Vulkan·WebGPU)로 코드를 짜거나 그래픽 카드로 계산만 돌릴 때 직접 만납니다.

상세

관공서 창구에 내는 신고서에는 인쇄된 양식이 있습니다. 1번 칸은 이름, 2번 칸은 날짜, 3번 칸은 서명처럼 칸마다 적을 것이 정해져 있습니다. 창구 직원은 양식을 외우고 있어서 서류를 받자마자 3번 칸으로 눈을 옮깁니다. 사람마다 적는 내용은 달라도 양식은 하나입니다.

이 절은 디스크립터 세트 레이아웃이 칸마다 무엇을 적는지, 그 틀을 누가 받아 쓰는지, 셰이더 코드와 어떻게 맞추는지를 봅니다. 의자 하나를 화면에 그리는 장면을 줄곧 예로 씁니다. 끝에서는 코드 한 조각과 다른 그래픽스 라이브러리가 쓰는 이름을 봅니다.

셰이더와 디스크립터 세트

GPU(Graphics Processing Unit, 그래픽 처리 장치)는 같은 계산을 동시에 아주 많이 돌리는 칩입니다. 화면의 점 수백만 개의 색을 한꺼번에 정하려고 만들었습니다. 이 칩을 얹은 판이 그래픽 카드입니다.

GPU 위에서 도는 작은 프로그램이 셰이더입니다. 의자 표면의 점 하나가 무슨 색이 될지 계산하는 코드가 셰이더입니다. 이 코드는 수많은 점에서 동시에 돕니다.

셰이더는 계산에 쓸 데이터를 바깥에서 받아 읽습니다. 이 데이터를 자원이라고 합니다. 의자라면 나뭇결 그림, 광택 값, 카메라 위치가 자원입니다.

셰이더 코드에는 자원의 이름 대신 번호 둘이 적힙니다. 「1번 세트의 0번 칸」처럼 적습니다. 그리기 전에 이 번호에 진짜 자원을 이어 두면 셰이더가 번호를 따라가 읽습니다.

번호와 자원을 잇는 것이 디스크립터입니다. 자원 하나가 메모리 어디에 있고 어떤 종류인지를 담은 작은 데이터입니다. 셰이더는 디스크립터를 거쳐 자원을 찾아갑니다.

디스크립터 여러 개를 한 벌로 묶어 한 번에 잇는 묶음이 디스크립터 세트입니다. 의자의 재질 세트에는 나뭇결 그림, 그림을 읽는 규칙, 광택 값의 디스크립터가 칸 셋에 나뉘어 듭니다.

앞에서 본 번호 둘이 여기서 이름을 얻습니다. 앞 번호는 세트를 고르는 세트 번호입니다. 뒤 번호는 세트 안의 칸을 고르는 바인딩 번호입니다. 「1번 세트의 0번 칸」은 세트 번호 1, 바인딩 번호 0 입니다.

세트를 찍어 내는 틀

세트를 만들기 전에 틀부터 정합니다. 몇 번 칸에 어떤 종류의 디스크립터가 몇 개 오는지 적은 틀이 디스크립터 세트 레이아웃입니다. 앞의 신고서로 치면 인쇄된 양식이 이 틀입니다. 줄여서 세트 레이아웃이라고 부릅니다.

틀 하나로 세트를 몇 개든 찍어 냅니다. 의자의 재질 세트와 책상의 재질 세트는 같은 세트 레이아웃으로 만든 두 세트입니다. 두 세트는 칸에 든 디스크립터만 다릅니다.

세트 레이아웃에는 자원이 없습니다. 나뭇결 그림이 메모리 어디에 있는지는 틀이 모릅니다. 틀은 「0번 칸에는 그림이 하나 온다」까지만 정합니다. 어느 그림인지는 찍어 낸 세트의 칸에 적힙니다.

칸마다 적는 네 가지

세트 레이아웃은 칸마다 네 가지를 적습니다. 바인딩 번호, 디스크립터 타입, 개수, 그 칸을 읽는 셰이더 단계입니다. 이 소절은 넷을 하나씩 보고 끝에서 의자의 재질 틀 한 벌로 모읍니다.

바인딩 번호는 칸의 번호입니다. 셰이더 코드가 「몇 번 칸」이라고 적는 번호가 이것입니다. 보통 0부터 차례로 매깁니다. 중간 번호를 비워 둬도 됩니다.

디스크립터 타입은 그 칸에 올 자원의 종류입니다. 종류마다 디스크립터의 꼴이 다릅니다. GPU 가 읽는 방법도 다릅니다. 그래서 칸마다 종류를 미리 정해 둡니다. 자주 쓰는 종류는 넷입니다.

디스크립터 타입 칸에 오는 자원 의자 그리기에서
텍스처 그림 데이터 나뭇결 그림
샘플러 텍스처에서 색을 읽어 오는 규칙 멀리서 볼 때 이웃한 색을 섞어 읽기
유니폼 버퍼 한 번 그리는 동안 모든 점이 같이 읽는 값 묶음 광택 값
스토리지 버퍼 셰이더가 읽고 쓸 수 있는 큰 값 목록 계산 결과를 받는 목록

개수는 그 칸에 드는 디스크립터의 수입니다. 보통은 1입니다. 같은 종류의 디스크립터를 칸 하나에 배열로 여럿 넣을 때 이 값을 늘립니다. 텍스처 여덟 장을 한 칸에 넣으면 셰이더는 그 칸 안에서 번호로 한 장을 골라 읽습니다.

칸을 읽는 셰이더 단계

그림 한 장은 셰이더 여럿을 차례로 거쳐 나옵니다. 먼저 정점 셰이더가 물체의 꼭짓점을 화면 위치로 옮깁니다. 이어서 프래그먼트 셰이더가 화면의 점마다 색을 정합니다. 이 차례 하나하나가 셰이더 단계입니다.

세트 레이아웃은 칸마다 어느 단계가 그 칸을 읽는지 적습니다. 카메라 위치는 꼭짓점을 옮길 때 쓰므로 정점 셰이더가 읽습니다. 나뭇결 그림과 광택 값은 색을 정할 때 쓰므로 프래그먼트 셰이더가 읽습니다. 적힌 단계만 그 칸을 읽을 수 있습니다.

카메라 위치는 재질이 아니라서 재질 세트에 넣지 않습니다. 이 예에서는 카메라 위치를 0번 세트에 따로 두고 재질 세트를 1번에 둡니다. 재질 칸 셋의 네 가지를 모으면 의자의 재질 틀은 이렇게 적힙니다.

바인딩 번호 디스크립터 타입 개수 읽는 셰이더 단계
0 텍스처 1 프래그먼트
1 샘플러 1 프래그먼트
2 유니폼 버퍼 1 프래그먼트

표 한 줄이 칸 하나입니다. 이 표가 세트 레이아웃이 담는 내용의 전부입니다.

틀을 받아 쓰는 두 곳

세트 레이아웃은 만들어진 뒤 두 곳에 넘겨집니다. 하나는 세트를 찍어 내는 곳입니다. 다른 하나는 그리는 설정을 만드는 곳입니다. 두 곳 다 칸 배치를 미리 알아야 제 일을 합니다.

첫째는 디스크립터 풀입니다. 디스크립터 풀은 세트를 담을 메모리를 미리 크게 잡아 둔 통입니다. 세트는 모두 이 통에서 나옵니다.

풀에서 빈 세트 하나를 받아 내는 일을 세트 할당이라고 합니다. 풀에 세트 레이아웃을 건네면 그 칸 배치대로 나뉜 빈 세트가 하나 나옵니다. 풀은 세트 레이아웃을 보고 세트 하나에 드는 몫을 셉니다. 재질 틀이면 텍스처 하나, 샘플러 하나, 유니폼 버퍼 하나 몫입니다.

재질 세트를 100 개 할당하려면 풀이 종류마다 100 개 몫을 가지고 있어야 합니다. 모자라면 할당이 실패합니다.

둘째는 파이프라인입니다. 파이프라인은 셰이더와 그리기 설정을 그리기 전에 한데 묶어 고정해 둔 객체입니다. 의자처럼 나무를 칠하는 셰이더를 쓰는 물체는 모두 같은 파이프라인으로 그립니다.

파이프라인은 세트 레이아웃을 바로 받지 않고 목록으로 받습니다. 이 파이프라인이 받을 세트 레이아웃들을 세트 번호 순서로 늘어놓은 목록이 파이프라인 레이아웃입니다. 목록에서 몇 번째에 놓였는지가 곧 세트 번호입니다. 의자 예에서는 카메라 위치를 담은 틀이 0번, 재질 틀이 1번에 놓입니다.

파이프라인을 만드는 일에는 드라이버가 끼어듭니다. 드라이버는 프로그램의 명령을 칩이 알아듣는 꼴로 바꿔 주는 소프트웨어입니다.

드라이버는 파이프라인을 만들 때 셰이더 코드를 GPU 의 기계어로 옮깁니다. 이렇게 옮기는 일이 컴파일입니다.

드라이버는 컴파일하면서 「1번 세트의 0번 칸」을 어떻게 읽을지를 기계어에 적어 넣습니다. 그러려면 그 칸에 무슨 종류가 오는지를 그때 알아야 합니다. 세트 레이아웃이 그 답을 줍니다.

그리기 직전에 세트 하나를 파이프라인의 세트 번호에 잇는 일을 세트 바인딩이라고 합니다. 세트를 그 번호에 끼운다고 보면 됩니다. 바인딩 번호가 세트 안 칸의 번호라면, 세트 바인딩은 세트 통째를 세트 번호에 잇는 일입니다.

읽는 방법은 컴파일할 때 기계어에 미리 들어가 있습니다. 그래서 그릴 때는 세트를 바인딩하기만 하면 됩니다. 바인딩한 세트의 칸이 가리키는 자원을 셰이더가 읽습니다.

flowchart TD
    L["세트 레이아웃 · 재질 틀"]
    POOL["디스크립터 풀"]
    PL["파이프라인 레이아웃 · 1번에 재질 틀"]
    P["파이프라인"]
    A["의자 재질 세트"]
    B["책상 재질 세트"]
    L -->|건넨다| POOL
    POOL -->|찍어 낸다| A
    POOL -->|찍어 낸다| B
    L -->|목록에 놓인다| PL
    PL -->|만들 때 넘긴다| P
    A -->|그릴 때 1번에 바인딩한다| P

그림은 틀 하나가 두 갈래로 넘겨졌다가 그릴 때 다시 만나는 모양을 보입니다. 한 갈래는 풀을 거쳐 세트가 됩니다. 다른 갈래는 파이프라인 레이아웃을 거쳐 파이프라인이 됩니다. 의자 재질 세트를 이 파이프라인의 1번에 바인딩할 수 있는 까닭은 둘이 같은 틀에서 나왔기 때문입니다.

같은 틀로 치는 기준

그릴 때 바인딩하는 세트는 파이프라인 레이아웃의 그 번호에 놓인 세트 레이아웃과 맞아야 합니다. 맞는다는 것은 같은 객체라는 뜻이 아닙니다. 칸마다 적은 내용이 모두 같으면 따로 만든 두 세트 레이아웃도 같은 틀로 칩니다.

의자를 그리는 파이프라인과 책상을 그리는 파이프라인이 있다고 합시다. 재질 틀은 파이프라인마다 따로 만들었습니다. 두 틀의 칸 배치가 같으면 의자 쪽 틀로 찍은 세트를 책상 쪽 파이프라인에 바인딩해도 됩니다.

칸 하나라도 다르면 다른 틀입니다. 바인딩 번호, 디스크립터 타입, 개수, 읽는 셰이더 단계 가운데 하나만 달라도 그렇습니다.

두 재질 틀의 차이 같은 틀로 치나
없다. 따로 만들었을 뿐이다 친다
2번 칸을 읽는 셰이더 단계가 다르다 안 친다
유리컵 재질이라 빛이 꺾이는 정도를 담는 3번 칸이 더 있다 안 친다

표의 둘째 줄처럼 자원 종류가 같아도 읽는 단계가 다르면 다른 틀입니다. 칸 배치를 정할 때 단계까지 맞춰 두면 세트를 파이프라인 여럿이 나눠 쓸 수 있습니다.

세트 레이아웃은 한 번 만들면 고칠 수 없습니다. 칸을 하나 더하려면 세트 레이아웃을 새로 만듭니다. 그 틀로 찍은 세트와 그 틀을 넣은 파이프라인 레이아웃과 파이프라인도 새로 만듭니다.

셰이더 코드와 맞추는 약속

셰이더 코드에도 칸 배치가 적힙니다. 세트 레이아웃과 셰이더 코드가 같은 칸 배치를 말해야 그림이 제대로 나옵니다. 이 소절은 의자의 재질 칸 셋을 셰이더 쪽과 프로그램 쪽에서 각각 적어 봅니다.

셰이더는 GLSL(OpenGL Shading Language, OpenGL 셰이딩 언어)로 적었습니다. set 은 세트 번호입니다. binding 은 바인딩 번호입니다.

glsl
layout(set = 1, binding = 0) uniform texture2D woodTex;
layout(set = 1, binding = 1) uniform sampler woodSampler;
layout(set = 1, binding = 2) uniform Material {
  float gloss;
};

세 줄이 앞의 재질 틀 표의 세 칸입니다. texture2D 는 텍스처 칸, sampler 는 샘플러 칸입니다. 번호와 종류가 표와 같습니다.

세 줄 모두에 uniform 이 붙어 있습니다. GLSL 에서 uniform 은 셰이더 바깥에서 받는 텍스처·샘플러·값 묶음에 두루 붙는 말입니다. 유니폼 버퍼 칸인지는 뒤에 중괄호 블록이 오는지로 가립니다. 셋째 줄의 Material { … } 이 유니폼 버퍼 칸입니다.

프로그램 쪽은 Vulkan 으로 적었습니다. Vulkan 은 프로그램이 GPU 를 다룰 때 부르는 함수 모음의 하나입니다. 이런 함수 모음을 그래픽스 API(Application Programming Interface, 응용 프로그램 인터페이스)라고 부릅니다.

C
#define FRAG VK_SHADER_STAGE_FRAGMENT_BIT

VkDescriptorSetLayoutBinding b[] = {
  {0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,  1, FRAG},
  {1, VK_DESCRIPTOR_TYPE_SAMPLER,        1, FRAG},
  {2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, FRAG},
};

중괄호 한 줄이 칸 하나입니다. 값은 차례로 바인딩 번호, 디스크립터 타입, 개수, 읽는 셰이더 단계입니다. SAMPLED_IMAGE 가 텍스처 칸입니다. FRAG 는 프래그먼트 셰이더 단계를 짧게 적으려고 맨 위에서 붙인 이름입니다.

C
VkDescriptorSetLayoutCreateInfo info = {
  .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
  .bindingCount = 3,
  .pBindings = b,
};
VkDescriptorSetLayout materialLayout;
vkCreateDescriptorSetLayout(device, &info, NULL, &materialLayout);

두 번째 조각은 칸 목록 b 를 넘겨 세트 레이아웃 객체를 만듭니다. device 는 프로그램이 GPU 에 명령을 내리려고 얻은 객체입니다. 만들어진 materialLayout 을 풀과 파이프라인 레이아웃에 넘깁니다.

셰이더 코드와 세트 레이아웃은 어긋날 수 있습니다. 셰이더는 1번 세트의 0번 칸을 텍스처로 읽습니다. 그런데 세트 레이아웃은 그 칸을 버퍼로 적었다고 합시다. Vulkan 은 이런 어긋남이 있을 때 결과를 보장하지 않습니다. 화면이 깨지거나 프로그램이 멈출 수 있습니다.

그래서 개발하는 동안에는 검증 레이어를 켜 둡니다. 검증 레이어는 Vulkan 호출을 가로채 규칙을 어겼는지 따져 알려 주는 도구입니다. 셰이더와 세트 레이아웃의 어긋남은 파이프라인을 만들 때 잡힙니다.

그래픽스 API 마다 부르는 이름

디스크립터 세트 레이아웃은 Vulkan 이 쓰는 이름입니다. 다른 그래픽스 API 에도 세트와 그 틀이 있습니다. 이름만 다릅니다.

API 묶음을 부르는 이름 틀을 부르는 이름
Vulkan 디스크립터 세트 디스크립터 세트 레이아웃
WebGPU 바인드 그룹 바인드 그룹 레이아웃
Direct3D 12 디스크립터 테이블 루트 시그니처 안에 적는다
Metal 아규먼트 버퍼 셰이더 코드의 구조체

WebGPU 는 웹 브라우저에서 GPU 를 다루는 API 입니다. 바인드 그룹 레이아웃도 칸마다 번호와 자원 종류와 그 칸을 보는 셰이더 단계를 적습니다.

Direct3D 12 에는 세트 레이아웃만 따로 만드는 객체가 없습니다. 칸 배치를 루트 시그니처라는 객체 안에 적습니다. 루트 시그니처는 Vulkan 의 파이프라인 레이아웃에 해당하는 목록입니다.

Metal 은 애플 기기에서 GPU 를 다루는 API 입니다. Metal 에는 틀을 따로 만드는 객체가 없습니다. 셰이더 코드에 구조체를 하나 선언하고 그 구조체의 필드 하나하나를 칸으로 씁니다. 아규먼트 버퍼는 그 구조체 모양대로 자원을 채운 버퍼입니다.

쓸 때와 안 쓸 때

백엔드 개발자가 세트 레이아웃을 손으로 만들 일은 드뭅니다. 게임 엔진이나 그래픽 라이브러리가 안쪽에서 대신 만듭니다. 직접 만나는 때는 Vulkan 이나 WebGPU 를 직접 다룰 때입니다.

GPU 로 계산만 돌릴 때도 만납니다. 계산을 맡는 컴퓨트 셰이더도 버퍼를 세트의 칸으로 받습니다. 두 값 목록을 더한다면 입력 목록 둘과 결과 목록 하나를 스토리지 버퍼 칸 셋으로 적습니다. 세 칸을 읽는 단계는 컴퓨트 셰이더 하나입니다.

파이프라인이 하나뿐인 작은 프로그램이면 WebGPU 에서는 틀을 손으로 안 만들어도 됩니다. 파이프라인을 만들 때 layout: "auto" 라고 적으면 셰이더 코드의 번호를 읽어 틀을 대신 만듭니다. 이렇게 만든 틀은 그 파이프라인 전용입니다. 다른 파이프라인과 세트를 나눠 쓸 수 없습니다.

물체마다 텍스처가 다르고 그 수가 많으면 칸 하나의 개수를 크게 잡습니다. 텍스처 수백 장을 한 칸의 배열로 넣고 셰이더가 번호로 골라 읽게 합니다. 이 방식을 바인드리스 렌더링이라고 합니다. GPU 와 드라이버가 지원해야 쓸 수 있습니다.

그릴 때마다 바뀌는 작은 값 하나는 세트 레이아웃에 칸을 두지 않습니다. 물체마다 다른 색 값 하나가 그렇습니다. 그런 값은 그리기 명령에 바로 싣는 푸시 상수로 보냅니다. 푸시 상수의 크기는 세트 레이아웃이 아니라 파이프라인 레이아웃에 적습니다.

관련 항목

디스크립터 세트 레이아웃을 이루는 구성 요소

바인딩 번호 · 디스크립터 타입 · 셰이더 단계 · 디스크립터 배열 · 불변 샘플러

디스크립터 세트 레이아웃으로 찍어 내는 묶음과 그 통

디스크립터 세트 · 디스크립터 · 디스크립터 풀 · 자원

디스크립터 세트 레이아웃을 받아 쓰는 파이프라인 객체

파이프라인 레이아웃 · 파이프라인 · 그래픽스 파이프라인 · 컴퓨트 파이프라인 · 렌더 파이프라인

디스크립터 세트 레이아웃의 칸이 가리키는 자원

텍스처 · 샘플러 · 버퍼 · 유니폼 버퍼 · 스토리지 버퍼 · 이미지 뷰

디스크립터 세트 레이아웃과 칸 배치를 맞추는 셰이더

셰이더 · 정점 셰이더 · 프래그먼트 셰이더 · 컴퓨트 셰이더 · GLSL · SPIR-V · 셰이더 리플렉션

디스크립터 세트 레이아웃을 다른 그래픽스 API 에서 대신하는 객체

바인드 그룹 레이아웃 · 바인드 그룹 · 루트 시그니처 · 디스크립터 테이블 · 아규먼트 버퍼

디스크립터 세트 레이아웃의 칸 대신 자원을 건네는 방식

푸시 상수 · 푸시 디스크립터 · 바인드리스 · 디스크립터 인덱싱 · 디스크립터 버퍼

디스크립터 세트 레이아웃을 읽어 처리하는 소프트웨어

디바이스 드라이버 · 검증 레이어 · 컴파일 · 게임 엔진

디스크립터 세트 레이아웃을 두는 그래픽스 API

Vulkan · WebGPU · Direct3D · Metal · 그래픽스 API

디스크립터 세트 레이아웃이 속하는 상위 분류

그래픽스 · 렌더링 · GPU · API

다른 이름: descriptor set layout · VkDescriptorSetLayout · 세트 레이아웃