사전 Vulkan
표준

Vulkan

gabury1고친 사람 github-actions[bot]

Vulkan 은 프로그램이 그래픽 카드에게 그림 그리기와 계산을 시킬 때 쓰는 약속입니다. 언제 메모리를 잡고 언제 명령을 보낼지까지 프로그램이 직접 지시하게 합니다. 드라이버가 알아서 챙겨 주던 일이 줄어든 만큼 코드는 길어집니다. 대신 여러 코어가 동시에 명령을 쌓을 수 있고 걸리는 시간이 고르게 나옵니다.

쉽고 빠른 이해

Vulkan 은 그래픽 카드에 그림과 계산을 시키는 함수 목록을 정한 약속입니다. 게임이 한 장면을 그릴 때, 보낼 명령을 미리 한 덩이로 적어 두었다가 그래픽 카드에 한꺼번에 밀어 넣습니다.

이게 없으면 드라이버가 프로그램의 속내를 매번 짐작해야 합니다. 짐작이 빗나가면 화면이 갑자기 끊기는데, 무엇 때문에 끊겼는지는 프로그램 쪽에서 알 수 없습니다.

어떻게 도나:

  1. 쓸 그래픽 카드를 고르고 명령을 받아 줄 통로를 엽니다
  2. 그릴 명령을 메모장 하나에 미리 적어 둡니다
  3. 그 메모장을 통로에 넣으면 그래픽 카드가 적힌 대로 실행합니다

대가도 있습니다. 메모리를 잡고 치우는 일과 앞뒤 순서를 맞추는 일을 프로그램이 다 맡습니다. 빠뜨려도 오류가 안 나고 화면만 조용히 깨지는 때가 많습니다.

그래서 Vulkan 은 그리기 명령이 많고, 화면 한 장을 그리는 데 걸리는 시간이 들쭉날쭉하면 안 되는 프로그램에 알맞습니다. 화면 몇 장만 그리면 되는 프로그램은 준비가 짧게 끝나는 다른 그래픽 API 를 고릅니다.

상세

Vulkan 은 그래픽과 계산을 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 시키는 API(Application Programming Interface, 응용 프로그램 인터페이스)의 규격입니다. GPU 는 그래픽 카드에 올라간 계산 칩입니다. 어떤 함수를 어떤 인자로 부르면 무엇이 일어나야 하는지를 문서 하나로 정합니다. 그 함수를 실제로 돌리는 코드는 그 문서에 들어 있지 않습니다.

식당에 비유하면 Vulkan 은 주문서 양식입니다. 양식은 무엇을 어느 칸에 적는지만 정하고 요리는 주방이 합니다. 다만 이 양식은 재료와 불 세기까지 손님이 적게 되어 있어서 적는 데 시간이 걸립니다.

이 절은 Vulkan 을 세 묶음으로 봅니다. 먼저 누가 문서를 쓰고 누가 구현하는지를 봅니다. 다음으로 프로그램이 직접 떠맡은 결정들을 하나씩 따라갑니다. 끝으로 이 규격을 언제 고르는지와 옆에 선 다른 그래픽 API 를 짚습니다.

규격과 구현

Vulkan 문서는 Khronos Group 이라는 업계 단체가 관리합니다. 이 단체는 함수와 동작만 정하고 코드는 내놓지 않습니다. Vulkan 은 설치하는 프로그램이 아니라 규격입니다.

구현은 그래픽 카드 회사가 합니다. 각 회사의 드라이버 안에 Vulkan 함수가 들어 있습니다. 드라이버는 운영체제와 하드웨어 사이에서 장치를 다루는 소프트웨어입니다.

프로그램이 Vulkan 함수를 부르면 그 호출은 설치된 드라이버로 가서 그래픽 카드가 알아듣는 명령으로 바뀝니다. 한 번 짠 코드가 회사가 다른 카드에서도 도는 것은 회사마다 이 문서에 맞춰 드라이버를 만들기 때문입니다.

드라이버에서 프로그램으로 넘어온 결정

앞선 세대의 그래픽 API 에서는 드라이버가 프로그램의 속내를 짐작했습니다. 메모리를 언제 잡고 언제 풀지, 쌓인 명령을 언제 실제로 보낼지를 드라이버가 정했습니다.

짐작은 대체로 맞지만 빗나갈 때가 문제입니다. 드라이버가 뒤에서 하는 일이 안 보여서, 화면이 끊겨도 어느 호출 때문인지 프로그램 쪽에서 알 수 없었습니다.

Vulkan 은 그 결정을 프로그램에 넘겼습니다. 메모리 할당과 명령 제출과 실행 순서를 프로그램이 직접 적고, 드라이버는 적힌 대로만 합니다.

넘겨받은 쪽은 할 일이 늘어납니다. 삼각형 하나를 띄우는 데도 카드를 고르고 통로를 열고 명령을 미리 적어 둘 곳을 만드는 준비가 먼저 필요합니다. 대신 준비가 끝난 뒤의 그리기는 드라이버가 끼어드는 몫이 적어 걸리는 시간이 고르게 나옵니다.

디바이스와 큐

Vulkan 은 그래픽 카드를 바로 부르지 않습니다. 먼저 인스턴스를 만듭니다. 인스턴스는 이 프로그램이 Vulkan 을 쓰겠다고 알리고 기계에 꽂힌 카드 목록을 받아 오는 출입구입니다.

인스턴스에 물어보면 물리 디바이스 목록이 나옵니다. 물리 디바이스는 기계에 실제로 있는 그래픽 카드 하나하나입니다. 메인보드에 붙어 나오는 내장 그래픽도 하나로 셉니다.

쓸 카드를 골랐으면 그 위에 논리 디바이스를 만듭니다. 논리 디바이스는 그 카드를 쓰려고 프로그램이 여는 통로입니다. 통로를 만들 때 어떤 기능을 켤지도 같이 정합니다.

명령은 통로가 아니라 큐에 넣습니다. 큐는 카드가 일감을 꺼내 가는 대기줄입니다. 대기줄은 성격이 갈려서 그리기를 받는 줄과 메모리 복사만 받는 줄이 따로 있기도 합니다. 성격이 같은 줄들의 묶음이 큐 패밀리입니다.

flowchart TD
    A["인스턴스 · Vulkan 을 쓰겠다는 선언"] --> B["물리 디바이스 · 꽂혀 있는 카드 목록"]
    B --> C["논리 디바이스 · 고른 카드로 여는 통로"]
    C --> D["그리기 큐"]
    C --> E["복사 전용 큐"]

위 두 칸은 기계에 이미 있는 것을 알아보는 단계입니다. 아래 세 칸은 프로그램이 직접 만드는 것입니다. 만든 통로와 대기줄은 프로그램이 끝날 때 프로그램이 치웁니다.

명령 버퍼

Vulkan 함수를 불렀다고 카드가 바로 움직이지는 않습니다. 그리기 명령은 명령 버퍼에 먼저 적힙니다. 명령 버퍼는 보낼 명령을 순서대로 적어 두는 메모장입니다.

C
vkBeginCommandBuffer(cmd, &bi); // 적기 시작
vkCmdBindPipeline(cmd, kind, pipe);
vkCmdDraw(cmd, 3, 1, 0, 0);     // 점 3개
vkEndCommandBuffer(cmd);        // 적기 끝
vkQueueSubmit(q, 1, &si, fence); // 제출

vkCmd 로 시작하는 함수는 그리라는 명령이 아니라 그리라고 적어 두라는 명령입니다. 카드가 일을 시작하는 때는 마지막 줄에서 이 명령 버퍼를 큐에 넣은 뒤입니다.

적는 일과 실행하는 일이 갈려서 얻는 것이 둘 있습니다. 하나는 장면이 바뀌지 않는 한 한 번 적은 명령 버퍼를 여러 번 다시 제출할 수 있다는 것입니다.

다른 하나는 스레드 여럿이 각자의 명령 버퍼에 동시에 적을 수 있다는 것입니다. 이쪽이 코어가 여럿인 기계에서 값이 큽니다. 앞선 세대에서는 그리기 호출이 한 통로로 몰려서 코어를 더 써도 명령을 더 빨리 만들지 못했습니다.

미리 컴파일해 넘기는 셰이더

셰이더는 그래픽 카드에서 돌며 점의 위치와 픽셀 색을 정하는 작은 프로그램입니다. Vulkan 은 이 프로그램을 소스 코드 문자열로 받지 않습니다. SPIR-V(Standard Portable Intermediate Representation, 표준 이식 중간 표현)라는 바이트코드로 받습니다.

바이트코드는 사람이 읽는 소스와 카드가 읽는 기계어의 중간 형태입니다. 사람이 손으로 쓰는 셰이더 언어는 따로 있습니다. GLSL(OpenGL Shading Language, OpenGL 셰이딩 언어)이나 HLSL(High Level Shader Language, 고수준 셰이더 언어)로 쓰고, 빌드할 때 컴파일러가 그것을 SPIR-V 로 내립니다.

flowchart TD
    subgraph 빌드할 때
        A["셰이더 소스 · GLSL 이나 HLSL"] --> B["SPIR-V 바이트코드"]
    end
    subgraph 실행할 때
        C["드라이버가 바이트코드를 받음"] --> D["그 카드의 기계어"]
    end
    B --> C

프로그램은 빌드 결과인 바이트코드 파일을 들고 다니다 실행 중에 드라이버로 넘깁니다. 드라이버는 그것을 자기 카드의 기계어로 내립니다.

컴파일을 빌드할 때로 옮기면 달라지는 것이 둘입니다. 셰이더 문법 오류를 사용자 기계가 아니라 개발자 기계에서 만납니다. 그리고 드라이버마다 셰이더 언어를 조금씩 다르게 해석하던 어긋남이 줄어듭니다.

미리 굳히는 파이프라인

그리기에 걸리는 설정은 파이프라인 하나로 묶입니다. 어떤 셰이더를 쓸지, 삼각형의 뒷면을 버릴지, 앞뒤 깊이를 비교할지 같은 것이 여기 다 들어갑니다.

파이프라인은 그 묶음을 미리 만들어 굳혀 둔 덩어리입니다. 굳힌다는 것은 만든 뒤에 못 바꾼다는 뜻입니다. 설정 하나를 바꾸려면 그 조합으로 파이프라인을 따로 하나 더 만들어 둬야 합니다.

이렇게 두면 드라이버가 그리는 동안 할 일이 줄어듭니다. 설정 조합에 맞는 카드용 코드를 파이프라인을 만드는 때에 미리 준비해 두기 때문입니다.

대신 그 준비에 시간이 걸립니다. 그리는 도중에 새 파이프라인을 처음 만들면 화면이 한 번 걸립니다. 그래서 쓸 조합은 프로그램이 뜰 때나 장면을 읽어 들일 때 미리 다 만들어 둡니다.

메모리 할당과 나눠 쓰기

Vulkan 에서 버퍼나 텍스처를 만들면 아직 메모리가 안 붙은 껍데기가 나옵니다. 카드에 어떤 메모리가 있는지 물어보고, 그중 하나를 골라 할당하고, 껍데기에 붙이는 일을 프로그램이 따로 합니다.

카드의 메모리는 성질에 따라 갈립니다. 카드만 빠르게 읽는 메모리가 있고, CPU(Central Processing Unit, 중앙 처리 장치)에서 직접 써 넣을 수 있는 메모리가 있습니다. 매 프레임, 곧 화면 한 장을 그릴 때마다 바뀌는 값은 써 넣기 쉬운 쪽에 두고, 한 번 올리고 계속 읽기만 하는 값은 카드가 빠르게 읽는 쪽에 둡니다.

할당을 데이터 하나마다 부르는 방식은 권하지 않습니다. 큰 덩어리를 한 번 잡아 두고 그 안을 프로그램이 쪼개 쓰는 방식이 보통입니다. 이 쪼개 쓰기를 대신해 주는 메모리 할당자 라이브러리를 얹기도 합니다.

순서를 맞추는 세 수단

그래픽 카드는 받은 명령을 앞의 것이 끝나기를 기다리지 않고 겹쳐서 처리합니다. 그래서 앞 명령이 쓴 값을 뒤 명령이 읽어야 한다면 그 사이에 기다리라는 표시가 있어야 합니다. Vulkan 은 이 표시를 대신 넣어 주지 않습니다.

기다림을 거는 수단은 셋이고, 누가 누구를 기다리느냐로 갈립니다.

수단 누가 누구를 기다리나
파이프라인 배리어 한 명령 버퍼 안에서 뒤 명령이 앞 명령을
세마포어 큐에 넣은 일감이 다른 일감을
펜스 CPU 가 카드의 일이 끝나기를

한 프레임을 그리는 코드는 대개 셋을 다 씁니다. 화면에 낼 그림 한 장을 얻고, 거기에 그리고, 화면으로 내보내는 세 걸음이 각각 다른 짝을 기다리기 때문입니다.

표시를 빠뜨려도 대개 오류가 안 납니다. 한 프레임 전 그림이 섞여 나오거나 어떤 기계에서만 깨집니다. 카드마다 명령을 겹치는 정도가 달라서 내 기계에서는 멀쩡하기도 합니다.

확장과 레이어

Vulkan 은 핵심 기능만 규격 본문에 두고 나머지는 확장으로 뺐습니다. 확장은 규격 본문 밖에 따로 정의된 함수와 상수 묶음입니다. 프로그램은 실행 중에 이 드라이버가 어떤 확장을 지원하는지 물어본 뒤 켭니다.

화면에 그림을 내보내는 기능도 확장입니다. 창을 띄우는 방식이 운영체제마다 달라서 핵심 규격은 창을 모릅니다. 대신 확장이 스왑체인을 줍니다. 스왑체인은 화면에 번갈아 내보낼 그림 여러 장을 미리 잡아 두고 돌려쓰는 묶음입니다.

또 하나 얹을 수 있는 것이 검증 레이어입니다. 레이어는 프로그램과 드라이버 사이에 끼워 넣는 층입니다. 검증 레이어를 켜면 인자가 규격에 안 맞거나 만든 것을 안 치웠을 때 바로 알려 줍니다.

Vulkan 함수는 인자가 틀려도 대부분 확인해 주지 않습니다. 확인을 기본으로 켜 두면 모든 프로그램이 그 비용을 내기 때문입니다. 그래서 개발하는 동안에는 레이어를 켜고 내보낼 때는 끕니다.

고를 때와 안 고를 때

Vulkan 이 값을 하는 곳은 그리기 명령이 아주 많고 프레임 시간이 고르게 나와야 하는 프로그램입니다. 게임 엔진과 실시간 3D 도구가 그렇습니다. 명령을 미리 적어 두는 방식이라 코어를 여럿 쓰는 쪽에서 특히 이득이 납니다.

반대로 화면 몇 장을 그리는 정도면 준비 코드가 본문보다 길어집니다. 이럴 때는 준비가 짧은 그래픽 API 를 고르거나, Vulkan 을 이미 감싸 둔 게임 엔진 위에서 작업합니다. 엔진을 쓰면 Vulkan 을 직접 만나는 일이 없습니다.

그림 없이 계산만 시킬 때도 고를 수 있습니다. Vulkan 은 그리기와 계산을 한 규격에서 다루기 때문입니다. 다만 계산만 필요하다면 그 일만 다루는 OpenCL(Open Computing Language, 오픈 컴퓨팅 언어)이나 CUDA(Compute Unified Device Architecture, 통합 연산 장치 아키텍처)가 준비 코드가 짧습니다.

나란한 그래픽스 API

OpenGL 은 Vulkan 보다 앞서 같은 단체가 낸 그래픽 규격입니다. 드라이버가 알아서 챙겨 주는 쪽이라 코드가 짧습니다. Vulkan 이 나온 뒤에도 사라지지 않았습니다. 짧게 끝나는 일에는 그쪽이 낫기 때문입니다.

Direct3D 와 Metal 은 운영체제 회사가 자기 플랫폼용으로 낸 API 입니다. 둘의 최신 판도 Vulkan 처럼 명령을 미리 적어 두고 메모리를 직접 잡는 설계를 씁니다.

애플 플랫폼에는 Vulkan 드라이버가 없습니다. 그래서 Vulkan 호출을 Metal 호출로 옮겨 주는 MoltenVK 같은 변환 계층을 얹어 씁니다. 코드는 Vulkan 으로 쓰고 실행만 그쪽에 맡기는 방식입니다.

WebGPU 는 브라우저 안에서 같은 일을 하려고 만든 규격입니다. 설계는 Vulkan 쪽을 따르되, 웹페이지가 남의 기계에서 도는 코드라는 점에 맞춰 안전 검사를 규격 안에 넣었습니다.

관련 항목

Vulkan 이 속하는 상위 분류

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

Vulkan 규격을 만들고 구현하는 주체

Khronos Group · GPU · 디바이스 드라이버 · GPU 드라이버 · Mesa

Vulkan 프로그램이 만들어 쓰는 객체

Vulkan 인스턴스 · 물리 디바이스 · 논리 디바이스 · 큐 · 큐 패밀리 · 명령 버퍼 · 명령 풀 · 디스크립터 셋 · 렌더 패스 · 프레임버퍼 · 스왑체인

Vulkan 이 그리기를 거치는 처리 단계

그래픽스 파이프라인 · 정점 셰이더 · 프리미티브 · 래스터화 · 프래그먼트 셰이더 · 깊이 테스트 · 블렌딩

Vulkan 이 실행 순서를 맞추는 수단

파이프라인 배리어 · 세마포어 · 펜스 · 이미지 레이아웃 · 메모리 배리어

Vulkan 이 GPU 메모리를 다루는 방법

메모리 힙 · 메모리 타입 · 버퍼 · 텍스처 · 메모리 할당자

Vulkan 이 셰이더를 받는 형식과 그 소스 언어

SPIR-V · GLSL · HLSL · 셰이더 · 컴퓨트 셰이더 · 바이트코드

Vulkan 이 규격 밖 기능을 붙이는 장치

Vulkan 확장 · 검증 레이어 · 레이 트레이싱 · 메시 셰이딩

Vulkan 을 대신할 수 있는 그래픽스 API

OpenGL · OpenGL ES · Direct3D · Metal · WebGPU · WebGL

Vulkan 코드를 다른 API 위에서 돌리는 변환 계층

MoltenVK · DXVK · ANGLE · Proton

Vulkan 을 감싸 쓰는 상위 도구

게임 엔진 · Unity · Unreal Engine · Godot

Vulkan 과 같은 일감을 받는 계산 전용 API

CUDA · OpenCL · SYCL · GPGPU

다른 이름: 불칸 · Vulkan API