사전 Metal
구현체

Metal

gabury1고친 사람 github-actions[bot]

Metal 은 애플 기기에서 도는 프로그램이 그래픽 칩에 그림 그리기와 계산을 직접 시키게 해 줍니다. 게임 한 장면을 그릴 명령을 프로그램이 한 덩이로 적어 두었다가 칩에 밀어 넣는 방식입니다. 애플이 함수 목록과 그것을 돌리는 코드를 함께 만들어 운영체제에 실어 내보냅니다. 그래서 다른 회사 기기에서는 같은 코드가 돌지 않습니다.

쉽고 빠른 이해

Metal 은 애플 기기의 그래픽 칩에 일을 시키는 함수 묶음입니다. 게임이 한 장면을 그릴 때, 보낼 명령을 메모장 하나에 적어 두었다가 칩에 한꺼번에 넘깁니다.

이게 없으면 명령을 한 통로로 하나씩 밀어 넣게 됩니다. 그러면 코어를 여럿 써도 명령을 더 빨리 만들지 못해, 칩이 놀고 있는데 화면이 끊깁니다.

어떻게 도나:

  1. 쓸 칩을 잡고 명령을 받아 줄 대기줄을 엽니다
  2. 그릴 명령을 메모장에 적습니다
  3. 메모장을 대기줄에 넣으면 칩이 적힌 대로 실행합니다

대가도 있습니다. 메모리를 어디에 둘지와 앞뒤 순서를 맞추는 일을 프로그램이 맡습니다. 그리고 이 코드는 애플 기기 밖으로 못 나갑니다.

상세

Metal 은 그래픽과 계산을 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 시키는 API(Application Programming Interface, 응용 프로그램 인터페이스)입니다. 그 API 를 돌리는 코드까지 함께 들어 있어서 프레임워크라고 부릅니다. 프레임워크는 프로그램이 불러다 쓰는 함수와 그 구현을 묶어 운영체제에 실어 놓은 꾸러미입니다.

여기서 "그래픽 칩"이라고 부른 것이 GPU 입니다. GPU 는 같은 계산을 수많은 점과 픽셀에 한꺼번에 먹이는 데 맞게 만든 칩입니다. 그림을 그리는 일과 그림과 상관없는 대량 계산을 둘 다 받습니다.

규격과 구현을 한 회사가 함께 갖는다

OpenGL 이나 Vulkan 은 업계 단체가 문서로 함수와 동작만 정하고, 구현은 그래픽 카드 회사들이 각자 드라이버에 넣습니다. 드라이버는 운영체제와 하드웨어 사이에서 장치를 다루는 소프트웨어입니다.

Metal 은 그 둘을 한 회사가 함께 갖습니다. 애플이 함수 목록을 정하고, 자기 칩에 맞는 구현을 만들고, 그것을 운영체제에 넣어 내보냅니다. 따로 받아서 까는 것이 아니라 기기에 이미 들어 있습니다.

flowchart TD
    subgraph K["OpenGL · Vulkan — 규격 하나에 구현 여럿"]
        KS["업계 단체가 정한 규격 문서"]
        KS --> KA["회사 A 드라이버"]
        KS --> KB["회사 B 드라이버"]
        KS --> KC["회사 C 드라이버"]
    end
    subgraph M["Metal — 규격과 구현이 한 자리"]
        MS["애플이 규격과 구현을 함께 만든다"]
        MS --> MO["운영체제에 실려 나간다"]
    end

구현이 하나면 아예 생기지 않는 골칫거리가 있습니다. 같은 함수를 회사마다 조금씩 다르게 해석해서 어떤 카드에서만 화면이 다르게 나오는 일입니다.

대신 고를 여지도 사라집니다. Metal 코드는 애플이 만든 기기 안에서만 돕니다. 이 포기가 이 프레임워크의 성격을 거의 다 정합니다.

명령이 칩까지 가는 길

Metal 함수를 불렀다고 칩이 곧바로 움직이지는 않습니다. 명령은 먼저 커맨드 버퍼에 적힙니다. 커맨드 버퍼는 보낼 명령을 순서대로 적어 두는 메모장입니다.

메모장에 글씨를 쓰는 일은 커맨드 인코더가 합니다. 인코더는 "삼각형을 그려라" 같은 사람 쪽 표현을 칩이 읽는 형식으로 바꿔 적는 물건입니다.

다 적은 메모장은 커맨드 큐에 넣습니다. 커맨드 큐는 칩이 일감을 꺼내 가는 대기줄입니다. 여기 넣는 때가 곧 칩이 일을 시작하는 때입니다.

이 셋은 전부 디바이스에서 나옵니다. 디바이스는 기기에 있는 GPU 하나를 가리키는 손잡이입니다. 프로그램은 먼저 디바이스를 잡고, 거기서 큐를 만들고, 큐에서 메모장을 얻습니다.

flowchart TD
    subgraph P["프로그램이 하는 일"]
        D["디바이스를 잡는다"] --> Q["커맨드 큐를 만든다"]
        Q --> B["커맨드 버퍼(메모장)를 얻는다"]
        B --> E["커맨드 인코더로 명령을 적는다"]
    end
    E --> S["커맨드 버퍼를 큐에 제출"]
    subgraph G["GPU 가 하는 일"]
        S --> R["적힌 명령을 실행한다"]
    end

그림의 위 네 칸은 아직 칩이 움직이지 않는 준비 단계입니다. 칩이 일을 시작하는 때는 제출한 뒤입니다. 그 뒤로 프로그램과 칩은 따로 돕니다.

코드로는 이 순서가 그대로 드러납니다.

swift
let dev = MTLCreateSystemDefaultDevice()!
let q = dev.makeCommandQueue()!   // 큐
let b = q.makeCommandBuffer()!    // 메모장
let e = b.makeRenderCommandEncoder(descriptor: pass)!
e.setRenderPipelineState(pso)     // 설정
e.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
e.endEncoding()
b.commit()                        // 제출

drawPrimitives 는 그리라는 명령이 아니라 그리라고 적어 두라는 명령입니다. 칩이 움직이는 때는 마지막 줄에서 메모장을 큐에 넣은 뒤입니다. 넘긴 값 가운데 pass 는 어디에 그릴지를 적은 설정이고, pso 는 아래 「그리기 설정을 묶어 굳힌 파이프라인 상태」에서 볼 설정 덩어리입니다.

적는 일과 실행하는 일을 가르면 얻는 것이 있습니다. 메모장 여러 개를 스레드 여럿이 동시에 적을 수 있습니다. 화면 한 장에 그리기 명령이 수천 개씩 들어가는 프로그램에서는 명령을 만드는 일 자체가 짐이라, 그 짐을 코어들이 나눠 집니다.

인코더 세 갈래

인코더는 받는 일감의 성격에 따라 갈립니다. 한 메모장 안에서 갈래를 바꿔 가며 쓸 수 있고, 갈래를 바꿀 때는 쓰던 인코더를 끝내고 새로 만듭니다.

갈래 받는 일감
렌더 화면이나 렌더 타깃에 그림을 그린다
컴퓨트 그림과 상관없는 계산을 시킨다
블릿 메모리에서 메모리로 데이터를 옮긴다

표의 렌더 타깃은 그린 결과가 담기는 그림판입니다. 화면일 수도 있고, 화면에 안 내보내고 다음 그리기에 쓰는 중간 이미지일 수도 있습니다.

컴퓨트 갈래가 있어서 Metal 은 그리기 전용이 아닙니다. 물리 계산이나 이미지 처리처럼 같은 연산을 데이터 수만 개에 먹이는 일을 GPU 에 넘길 수 있습니다. 이때 넘기는 계산 프로그램이 컴퓨트 셰이더입니다.

블릿 갈래는 그림을 만들지 않고 데이터를 나르는 일만 받습니다. 예를 들면 디스크에서 읽어 온 텍스처를 GPU 쪽 메모리로 옮기는 일입니다. 텍스처는 그릴 면에 입히는 이미지 데이터입니다.

셰이더는 따로 쓰고 미리 컴파일한다

셰이더는 GPU 에서 돌며 점의 위치와 픽셀의 색을 정하는 작은 프로그램입니다. 앱 코드와는 다른 언어로 씁니다.

그 언어가 MSL(Metal Shading Language, 메탈 셰이딩 언어)입니다. C++ 를 바탕으로 만들어서 문법이 낯설지 않고, GPU 에 없는 기능은 빠져 있습니다.

셰이더는 앱을 빌드할 때 함께 컴파일해 라이브러리 파일 하나로 묶습니다. 앱은 그 파일을 들고 다니다 실행 중에 필요한 셰이더를 이름으로 꺼내 씁니다.

컴파일을 빌드할 때로 옮기면 셰이더 문법 오류를 사용자 기기가 아니라 개발자 기기에서 만납니다. 실행 중에 셰이더 소스를 넘겨 그때 바로 컴파일하는 길도 열려 있지만, 그쪽은 앱이 뜬 뒤에 시간을 씁니다.

그리기 설정을 묶어 굳힌 파이프라인 상태

한 번 그릴 때 정해야 하는 것이 많습니다. 어떤 셰이더를 쓸지, 점의 좌표를 메모리에서 어떤 모양으로 읽을지, 결과를 어떤 형식의 화면 버퍼에 쓸지 같은 것입니다.

Metal 은 이 설정을 그릴 때마다 하나씩 받지 않습니다. 파이프라인 상태 객체 하나로 미리 묶어 만들어 두고, 그릴 때는 그 덩어리를 지정합니다. 굳힌다는 것은 만든 뒤에 못 바꾼다는 뜻이라, 설정 하나가 다르면 그 조합으로 객체를 하나 더 만들어 둡니다.

이렇게 두면 그리는 동안 할 일이 줄어듭니다. 그 조합에 맞는 칩용 코드를 객체를 만드는 때에 미리 준비해 두기 때문입니다.

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

메모리를 어디에 둘지 고른다

GPU 가 쓸 데이터는 버퍼나 텍스처에 담깁니다. 버퍼는 형식을 정하지 않고 바이트를 그대로 담는 메모리 덩어리입니다. 텍스처 쪽은 가로세로와 픽셀 형식이 정해져 있어서 GPU 가 이미지로 읽습니다.

만들 때 스토리지 모드를 같이 정합니다. 스토리지 모드는 이 데이터를 CPU(Central Processing Unit, 중앙 처리 장치)와 GPU 중 누가 볼 수 있는 곳에 둘지를 고르는 값입니다.

모드 누가 보나
공유 CPU 와 GPU 가 같은 메모리를 본다
전용 GPU 만 본다. CPU 는 블릿으로만 넣는다
비저장 칩 안에서만 살고 메모리에 안 내려간다

애플이 자기 칩을 쓰는 기기에서는 CPU 와 GPU 가 물리 메모리를 함께 씁니다. 이런 기기에서 공유 모드를 고르면 CPU 가 쓴 값을 복사 없이 GPU 가 바로 읽습니다.

외장 그래픽 카드가 달린 기기는 다릅니다. 카드가 자기 메모리를 따로 갖고 있어서, 프로그램이 쓴 값이 GPU 에 닿으려면 어느 시점엔가 복사가 일어납니다. 같은 코드라도 기기에 따라 드는 비용이 달라지는 대목입니다.

GPU 칩 안에는 작은 임시 저장 공간이 따로 있습니다. 비저장 모드는 값을 거기서만 쓰고 바깥 메모리로 내보내지 않습니다. 한 번 그리는 동안만 쓰고 버리는 중간 결과에 씁니다. 깊이 값처럼 그리는 중에만 필요하고 끝나면 볼 일이 없는 데이터가 그렇습니다.

아래 그림은 같은 데이터가 기기에 따라 어디에 앉는지를 보입니다.

flowchart TD
    subgraph U["애플 칩 기기 — 메모리가 하나다"]
        UC["CPU"] --> UM["시스템 메모리 · 공유 모드"]
        UG["GPU"] --> UM
    end
    subgraph X["외장 그래픽 카드 기기 — 메모리가 둘이다"]
        XC["CPU"] --> XM["시스템 메모리"]
        XM -->|"블릿으로 복사"| XV["카드 메모리 · 전용 모드"]
        XG["GPU"] --> XV
    end
    subgraph T["GPU 칩 안"]
        TT["임시 저장 공간 · 비저장 모드 — 메모리에 안 내려간다"]
    end

끝나기를 기다리고 순서를 맞추는 수단

메모장을 큐에 넣고 나면 프로그램은 바로 다음 줄로 넘어갑니다. GPU 의 일은 그때부터 따로 돕니다. 그래서 그 일이 끝났는지를 따로 물어봐야 하는 때가 생깁니다.

가장 흔한 방법은 커맨드 버퍼에 끝났을 때 부를 함수를 걸어 두는 것입니다. GPU 가 그 메모장을 다 처리하면 걸어 둔 함수가 불립니다. 결과를 CPU 로 읽어 와야 할 때 이 방법을 씁니다.

제출한 뒤 멈춰 기다리는 방법도 있습니다. 다만 기다리는 동안 CPU 는 아무것도 못 하므로, 매 프레임마다 쓰면 화면 한 장을 그리는 시간이 그만큼 늘어납니다. 프레임은 화면에 내보내는 그림 한 장입니다.

GPU 는 받은 일감을 앞의 것이 끝나기를 기다리지 않고 겹쳐서 처리합니다. 그래서 앞 일감이 쓴 메모리를 뒤 일감이 읽어야 한다면 그 사이에 기다리라는 표시가 있어야 합니다.

그 표시를 거는 물건이 펜스와 이벤트입니다. 둘 다 "여기까지 끝나면 다음으로 가라"를 적어 두는 수단입니다. 어디까지가 한 짝인지는 프로그램이 정합니다. 표시를 빠뜨려도 오류가 안 나고 어떤 기기에서만 화면이 조용히 깨지기도 합니다.

제출한 뒤의 시간축을 그리면 이렇습니다.

sequenceDiagram
    participant 앱 as 프로그램
    participant 큐 as 커맨드 큐
    participant GPU as GPU
    앱->>큐: 일감 A 를 제출
    큐-->>앱: 바로 다음 줄로 넘어간다
    앱->>큐: 일감 B 를 제출
    큐-->>앱: 바로 다음 줄로 넘어간다
    큐->>GPU: A 를 넘긴다
    큐->>GPU: B 를 넘긴다
    Note over GPU: A 와 B 가 겹쳐 돈다
    Note over GPU: 펜스 — A 가 여기까지 끝나야 B 가 그 메모리를 읽는다
    GPU-->>앱: A 가 끝났다고 알린다

포기한 것 — 애플 기기 밖

Metal 은 이식성을 포기했습니다. 다른 회사의 운영체제에는 Metal 함수를 받아 줄 구현이 없습니다.

그래서 여러 플랫폼을 함께 내는 프로그램은 대개 Metal 을 직접 부르지 않습니다. Unity 나 Unreal Engine 같은 게임 엔진 위에서 작업하면 엔진이 플랫폼마다 다른 API 를 골라 부릅니다.

Vulkan 으로 짠 코드를 애플 기기에서 돌려야 할 때는 MoltenVK 같은 변환 계층을 얹습니다. Vulkan 호출을 Metal 호출로 옮겨 주는 층이라, 코드는 Vulkan 으로 두고 실행만 Metal 에 맡깁니다.

애플 기기 안에서도 모든 앱이 Metal 을 직접 부르지는 않습니다. Core Image 나 SceneKit 처럼 사진 필터와 3D 장면을 다루는 상위 프레임워크가 그 아래에서 Metal 을 부르고, 앱은 그 프레임워크만 씁니다.

앱이 Metal 에 닿는 길은 이렇게 셋입니다.

flowchart TD
    subgraph A["앱"]
        A1["직접 부르는 앱"]
        A2["엔진이나 상위 프레임워크를 쓰는 앱"]
        A3["Vulkan 으로 짠 앱"]
    end
    subgraph L["사이에 얹히는 층"]
        L1["게임 엔진 · 상위 프레임워크"]
        L2["MoltenVK"]
    end
    A1 --> MM["Metal"]
    A2 --> L1
    A3 --> L2
    L1 --> MM
    L2 --> MM
    MM --> GG["GPU"]

고를 때와 안 고를 때

Metal 을 직접 부르는 쪽이 값을 하는 곳은 애플 기기만 대상으로 하는 프로그램입니다. 그중에서도 그리기 명령이 아주 많은 쪽입니다. 게임과 실시간 3D 도구가 그렇습니다.

한 장면에 들어가는 명령을 여러 코어가 나눠 적을 수 있습니다. 그릴 때 정할 것을 파이프라인 상태에 미리 굳혀 두기 때문에 그리는 도중에 새로 준비할 일이 적습니다. 그래서 프레임 시간이 고르게 나옵니다.

그림 없이 계산만 시킬 때도 고를 수 있습니다. 그리기와 계산을 한 API 에서 다루기 때문에, 이미지 처리 결과를 그대로 화면에 그리는 일을 중간 복사 없이 이어 붙일 수 있습니다.

반대로 화면 몇 장을 그리는 정도라면 준비 코드가 본문보다 길어집니다. 디바이스를 잡고 파이프라인 상태를 만들고 메모리를 배치하는 일이 삼각형 하나에도 그대로 필요합니다. 이럴 때는 상위 프레임워크나 엔진에 맡기는 쪽으로 갑니다.

여러 플랫폼을 함께 낼 때도 직접 부르지 않습니다. 같은 장면을 그리는 코드를 플랫폼 수만큼 따로 들고 있어야 하기 때문입니다.

고르는 순서를 갈림으로 적으면 이렇습니다.

flowchart TD
    Q1{"여러 플랫폼을 함께 내나"}
    Q1 -->|"예"| E1["엔진에 맡긴다"]
    Q1 -->|"아니오"| Q2{"그리기 명령이 아주 많나"}
    Q2 -->|"예"| E2["Metal 을 직접 부른다 · 게임 · 실시간 3D 도구"]
    Q2 -->|"아니오"| E3["상위 프레임워크에 맡긴다 · 화면 몇 장"]

관련 항목

Metal 이 속하는 상위 분류

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

Metal 이 실려 나가는 운영체제

macOS · iOS · iPadOS · tvOS · visionOS

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

MTLDevice · 커맨드 큐 · 커맨드 버퍼 · 커맨드 인코더 · 파이프라인 상태 객체 · 렌더 패스 · 아규먼트 버퍼 · 힙

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

그래픽스 파이프라인 · 정점 셰이더 · 프리미티브 · 래스터화 · 프래그먼트 셰이더 · 깊이 테스트 · 블렌딩 · 컬링 · 렌더 타깃 · 프레임버퍼

Metal 이 셰이더를 받는 형식과 그 언어

MSL · 셰이더 · 컴퓨트 셰이더 · GLSL · HLSL · 셰이더 컴파일러

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

버퍼 · 텍스처 · 스토리지 모드 · 통합 메모리 · 타일 메모리 · 샘플러

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

펜스 · 이벤트 · 세마포어 · 메모리 배리어

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

OpenGL · OpenGL ES · Vulkan · Direct3D · WebGPU · WebGL

Metal 과 다른 API 사이를 잇는 변환 계층

MoltenVK · ANGLE · SPIRV-Cross · SPIR-V

Metal 을 감싸 쓰는 상위 도구

게임 엔진 · Unity · Unreal Engine · Godot · Core Image · SceneKit · RealityKit

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

CUDA · OpenCL · SYCL · 컴퓨트 파이프라인

Metal 이 도는 하드웨어와 그 부품

GPU · CPU · 디바이스 드라이버 · 프레임 · 스레드

다른 이름: Metal API · 메탈 · Apple Metal