사전 SPIR-V
포맷

SPIR-V

gabury1고친 사람 github-actions[bot]

SPIR-V 는 그래픽 카드에서 돌릴 작은 프로그램을 미리 번역해 둡니다. 사람이 셰이더 언어로 쓴 소스는 빌드할 때 SPIR-V 로 한 번 바뀝니다. 프로그램을 돌릴 때 그래픽 드라이버는 소스 대신 그 결과를 받아 읽습니다. 소스를 해석하는 일이 실행할 때에서 빌드할 때로 옮겨 간 것입니다.

쉽고 빠른 이해

SPIR-V 는 그래픽 카드용 프로그램을 숫자의 줄로 적어 두는 규칙입니다. 셰이더 언어로 쓴 소스를 빌드하면 .spv 라는 이진 파일이 나옵니다. 프로그램은 그 파일을 그래픽 드라이버에 그대로 넘깁니다.

예전에는 드라이버가 셰이더 소스를 직접 읽어 컴파일했습니다. 드라이버마다 컴파일러가 달라서 같은 소스가 어떤 기계에서는 돌고 어떤 기계에서는 오류를 냈습니다. 그 오류를 만나는 곳도 개발자의 책상이 아니라 사용자의 기계였습니다.

어떻게 도나:

  1. 빌드할 때 셰이더 소스를 컴파일해 SPIR-V 파일로 만듭니다
  2. 그 파일을 프로그램과 함께 배포합니다
  3. 실행할 때 드라이버가 그것을 읽어 그래픽 카드의 기계어로 마무리합니다

그래픽 표준 가운데는 셰이더를 SPIR-V 로만 받는 것도 있습니다. 안 받는 플랫폼에는 도구로 글자 소스로 되돌려 넘깁니다.

대가는 둘입니다. 이진 파일이라 편집기로 열어도 읽히지 않습니다. 들여다보려면 도구로 글자 형태로 되돌려야 합니다. 그리고 마지막 번역은 여전히 드라이버가 하므로 기계마다 결과가 갈릴 여지가 남습니다.

상세

이름이 말하는 세 가지

SPIR-V 는 Standard Portable Intermediate Representation 의 줄임말입니다. 세 낱말이 하나씩을 말합니다.

표준은 한 회사가 아니라 크로노스 그룹이라는 단체가 규격을 정해 공개한다는 뜻입니다. 이식 가능은 특정 회사의 그래픽 카드나 특정 운영체제에 묶이지 않는다는 뜻입니다.

남은 중간 표현이 SPIR-V 의 성격을 정합니다. 중간 표현은 컴파일러가 사람이 쓴 소스와 칩이 읽는 기계어 사이에 끼워 두는 형태입니다. 소스는 사람에게 맞춰져 있습니다. 기계어는 칩마다 다릅니다.

가운데에 공통 형태를 하나 두면 번역기 수가 곱에서 합으로 줄어듭니다. 셰이더 언어 셋과 그래픽 카드 넷을 놓고 보면 이렇습니다.

flowchart TD
    subgraph 앞["셰이더 언어 셋"]
        L1["언어 하나"]
        L2["언어 둘"]
        L3["언어 셋"]
    end
    L1 --> S["SPIR-V · 가운데 하나"]
    L2 --> S
    L3 --> S
    subgraph 뒤["그래픽 카드 넷"]
        C1["칩 하나"]
        C2["칩 둘"]
        C3["칩 셋"]
        C4["칩 넷"]
    end
    S --> C1
    S --> C2
    S --> C3
    S --> C4

가운데가 없으면 언어 셋과 칩 넷을 하나씩 짝지어야 해서 번역기가 열둘 필요합니다. 가운데를 하나 두면 앞에서 셋, 뒤에서 넷, 모두 일곱으로 끝납니다. 언어가 하나 늘어도 새로 만들 번역기는 하나뿐입니다.

빌드할 때 한 번 번역해 두고 실행할 때는 그것만 읽는 짜임은 바이트코드를 쓰는 언어들과 같습니다.

셰이더 소스와 기계어 사이

셰이더는 그래픽 카드에서 도는 작은 프로그램입니다. 점의 위치를 옮기거나 픽셀 하나의 색을 정하는 계산을 맡습니다. 사람은 이 프로그램을 GLSL(OpenGL Shading Language, OpenGL 셰이딩 언어)이나 HLSL(High Level Shader Language, 고수준 셰이더 언어) 같은 셰이더 언어로 씁니다.

SPIR-V 는 그 소스와 GPU(Graphics Processing Unit, 그래픽 처리 장치)의 기계어 사이에 놓입니다. 번역이 두 번으로 갈리고 두 번 사이에 시간 간격이 생깁니다.

앞의 번역은 개발자가 빌드할 때 끝냅니다. 뒤의 번역은 사용자 기계의 그래픽 드라이버(디바이스 드라이버)가 합니다. 아래에서는 이것을 드라이버라고만 적습니다.

flowchart TD
    A["셰이더 소스 · GLSL 이나 HLSL"] --> B["셰이더 컴파일러 · 빌드할 때 돈다"]
    B --> C["SPIR-V 모듈 · .spv 파일"]
    C --> D["그래픽 드라이버 · 실행할 때 돈다"]
    D --> E["그래픽 카드의 기계어"]

가운데 칸이 프로그램과 함께 배포되는 것입니다. 셰이더 소스는 배포에 들어가지 않습니다.

드라이버에서 컴파일러를 걷어낸 까닭

셰이더 언어는 문법이 제법 큽니다. 드라이버가 소스를 직접 받으려면 드라이버마다 그 언어의 컴파일러를 하나씩 품어야 했습니다.

같은 일을 하는 컴파일러가 여럿이면 해석이 조금씩 갈립니다. 한 기계에서 잘 돌던 셰이더가 다른 기계에서 오류를 냈고, 그 사실은 개발자가 아니라 사용자가 먼저 만났습니다. 컴파일에 드는 시간도 프로그램이 도는 중에 들어갔습니다.

번역을 둘로 가르면 이 셋이 자리를 옮깁니다.

드라이버가 소스를 읽던 방식 SPIR-V 를 받는 방식
셰이더 언어를 해석하는 쪽 기계마다 다른 드라이버 개발자가 고른 컴파일러 하나
해석하는 때 프로그램이 도는 중 빌드할 때
문법 오류를 만나는 곳 사용자 기계 빌드

드라이버가 할 일도 줄어듭니다. 문법을 해석하는 대신 이미 명령으로 쪼개진 것을 읽어 자기 칩의 명령으로 옮기기만 하면 됩니다.

32비트 워드로 이어진 줄

SPIR-V 파일은 32비트 워드가 줄줄이 이어진 것입니다. 이 파일 하나를 모듈이라고 부릅니다. 워드는 바이트 넷을 한 덩이로 묶어 세는 단위입니다. 글자도 구분 기호도 없어서 읽는 도구는 처음부터 네 바이트씩 끊어 숫자로 읽습니다.

앞의 다섯 워드가 헤더이고 그 뒤로는 명령만 이어집니다.

block-beta
  columns 5
  h1["매직 넘버"] h2["규격 판"] h3["만든 도구"] h4["번호 상한"] h5["예약"]
  i1["명령 · 명령 · 명령 · 파일 끝까지"]:5

첫째 칸의 매직 넘버는 이 파일이 SPIR-V 임을 알리는 고정 값입니다. 첫 워드만 보고도 엉뚱한 파일을 받았다는 것을 알 수 있습니다.

둘째 칸과 셋째 칸은 이 모듈이 어느 규격 판을 따르는지와 어느 도구가 만들었는지를 적습니다. 넷째 칸의 번호 상한은 다음 소절에서 풉니다. 다섯째 칸(그림의 예약)은 나중을 위해 비워 둡니다.

명령은 길이가 제각각입니다. 그래서 명령의 첫 워드가 자기 길이를 같이 답니다. 서른두 비트가 반으로 갈립니다.

packet-beta
0-15: "명령 코드"
16-31: "몇 워드짜리인가"

아래 열여섯 비트가 무슨 명령인지를 담습니다. 위 열여섯 비트가 그 명령이 워드 몇 개를 차지하는지를 담습니다.

길이가 앞에 붙어 있으면 읽는 도구는 모르는 명령을 만나도 그만큼 건너뛰고 다음 명령으로 갈 수 있습니다.

block-beta
  columns 10
  a["명령 · 3워드"]:3 b["모르는 명령 · 5워드"]:5 c["명령 · 2워드"]:2
  space:3 d["다섯 워드를 건너뛴다"]:5 space:2

가운데 명령을 못 알아봐도 길이만 읽으면 그다음 명령의 첫 워드로 바로 갈 수 있습니다. 규격에 명령이 나중에 더 붙어도 옛 도구가 파일 전체를 포기하지 않아도 되는 까닭입니다.

결과마다 붙는 번호

SPIR-V 안에서는 타입도 상수도 계산 결과도 저마다 번호를 하나씩 받습니다. 뒤 명령은 값을 다시 적지 않고 그 번호로 가리킵니다. 한 번 번호가 붙은 결과에는 다른 값을 덮어쓰지 않습니다.

이진 파일이지만 도구로 글자 형태로 되돌려 볼 수 있습니다. 되돌린 모습은 이렇습니다.

%float = OpTypeFloat 32          // 실수 타입
%half = OpConstant %float 0.5    // 상수 0.5
%sum = OpFAdd %float %half %half // 1.0

세 줄이 서로를 번호로 가리킵니다. 둘째 줄은 첫째 줄의 %float 를 타입으로 씁니다. 셋째 줄은 그 타입과 둘째 줄의 %half 를 가져다 더합니다.

앞에 붙은 %float 같은 이름표가 곧 번호입니다. 사람이 읽으라고 이름처럼 보이게 적어 준 것입니다. 파일 안에서는 숫자입니다.

헤더의 번호 상한은 그 숫자가 어디까지 가는지 미리 알려 주는 값입니다. 드라이버는 파일을 다 읽기 전에 번호를 담을 표의 크기를 잡을 수 있습니다.

모듈이 스스로 밝히는 요구 기능

그래픽 카드가 할 수 있는 일은 기계마다 다릅니다. 64비트 실수 연산이 그런 기능입니다. 모든 그래픽 카드가 그 연산을 하지는 못합니다. 그래서 모듈은 헤더 다음 명령 첫머리에서 자기가 쓰는 기능을 하나씩 선언합니다.

드라이버는 그 선언만 읽고도 이 모듈을 돌릴 수 있는지 판정합니다. 못 받는 기능이 하나라도 걸리면 모듈 전체를 거부합니다. 모르는 명령을 만나 중간에 멈추는 것보다 먼저 거절하는 편이 다루기 쉽습니다.

덕분에 규격 하나로 여러 갈래를 묶을 수 있습니다. 그림을 그리는 Vulkan과 계산만 시키는 OpenCL이 같은 형태를 쓰되 서로 받는 명령의 범위가 다릅니다.

SPIR-V 의 표현 한계

SPIR-V 는 완성된 기계어가 아닙니다. 그래픽 카드의 명령어는 만든 회사마다 세대마다 다릅니다. 마지막 번역은 드라이버 몫으로 남습니다.

그래서 프로그램을 처음 돌릴 때 번역하는 시간이 한 번 듭니다. 그 결과를 저장해 두었다가 다음 실행에 다시 쓰는 길을 그래픽 표준들이 따로 둡니다.

변수와 함수의 이름도 SPIR-V 에 꼭 필요하지는 않습니다. 이름을 붙여 두는 명령이 따로 있습니다. 그것을 떼어 내면 모듈은 그대로 돌지만 되돌려 읽었을 때 번호만 남습니다. 어느 셰이더 언어로 썼는지도 대개 지워집니다.

사람이 열어 읽을 수 없다는 것도 내주는 값입니다. 텍스트 소스는 편집기로 열면 되지만 이 모듈은 도구를 거쳐야 보입니다. 대신 파일이 작아집니다. 드라이버가 문법을 해석하지 않아도 됩니다.

SPIR-V 를 받는 표준과 되돌리는 도구

Vulkan 은 셰이더를 SPIR-V 로만 받습니다. 소스 문자열을 넘기는 길이 아예 없습니다. OpenCL 처럼 그림을 그리지 않고 계산만 시키는 표준도 프로그램을 SPIR-V 로 넘깁니다.

OpenGL은 오래 소스를 직접 받아 왔습니다. 나중 판에서 이 모듈도 받는 길을 열었습니다.

방향을 거꾸로 돌리는 도구도 있습니다. SPIRV-Cross 같은 도구는 SPIR-V 모듈을 읽어 다른 셰이더 언어의 소스로 내려놓습니다. SPIR-V 를 안 받는 플랫폼에도 같은 셰이더를 쓸 수 있게 됩니다.

그래서 셰이더를 한 언어로 써 두고 SPIR-V 로 모은 다음, 플랫폼마다 필요한 형태로 다시 퍼뜨리는 방식이 자주 쓰입니다.

flowchart TD
    A["셰이더 언어 하나"] --> B["SPIR-V 모듈"]
    B --> C["Vulkan · OpenGL 은 그대로 받는다"]
    B --> D["SPIRV-Cross 로 되돌린다"]
    D --> E["그 플랫폼의 셰이더 언어"]

한 갈래는 모듈을 그대로 넘기고 다른 갈래는 글자 소스로 되돌려 넘깁니다. 가운데 형태를 하나 두는 값이 여기서 드러납니다.

관련 항목

앞에서 번역해 넣는 셰이더 언어

셰이더 · GLSL · HLSL · WGSL · MSL · 셰이더 언어

모듈을 셰이더 입구로 받는 그래픽 표준

Vulkan · OpenGL · OpenCL

모듈을 만들고 되돌리는 도구

셰이더 컴파일 · SPIRV-Cross · glslang · 컴파일러 · 크로스 컴파일

모듈이 지나가는 실행 경로

GPU · 디바이스 드라이버 · 그래픽스 파이프라인 · 정점 셰이더 · 프래그먼트 셰이더 · 지오메트리 셰이더 · 컴퓨트 셰이더

이 규격이 속하는 상위 분류

중간 표현 · 바이트코드 · 정적 단일 할당 · 가상 명령어 집합 · 포맷

같은 몫을 다른 형태로 푸는 이웃

DXIL · Direct3D · Metal · WebGPU · LLVM IR

규격을 세운 단체와 그 이웃 규격

크로노스 그룹 · OpenGL ES · glTF · EGL

다른 이름: SPIRV · spir-v · Standard Portable Intermediate Representation · SPIR-V 모듈