- GPU 프로그래밍 API
- 그래픽과 컴퓨팅 모두에 활용 가능
- A7 이상에서 사용 가능
- 미리 컴파일된 셰이더
- 멀티스레딩에 적용 가능
- 배경
- 모든 draw call은 각자의 상태 벡터를 필요로 한다.
- 상태 벡터를 바꾸는 것은 CPU 입장에서는 비싼 연산이다
- 소프트웨어에서 하드웨어쪽으로 연산을 옮길 필요가 있다.
- 메탈은 더 많은 draw call을 빠르게 처리할 수 있다(약 10배)
- 메탈 이전에는 표준적으로 OpenGL, OpenCL이 사용됨
- 여러 하드웨어에서 돌아가는 표준이기 때문에 최적화하기 힘듦
- 그래서 메탈은 OS와 하드웨어와 함께 통합됨
- 메탈 디자인 철학
- 가능한한 얇은 API
- 모던한 GPU 기능들을 사용
- 비싼 연산은 최대한 덜하는 방법으로
- 예측 가능한 퍼포먼스
- 명시적인 커맨드 제출
- CPU의 동작에서도 최적화
- 앱에서 사용되는 여러 API들의 포지션
- 씬 그래프: SceneKit, SpriteKit
- 2D 그래픽과 이미지 처리: Core Animation, Core Image, Core Graphics
- 표준 기반 3D 그래픽: OpenGL ES
- 고효율 GPU 접근: Metal
- 어떻게 이걸하는가?
- 많은 게임들이 타겟으로 하는 30FPS의 경우, 대략 프레임당 33.3ms안에 처리가 되어야 한다.
- CPU의 작업은 다음 프레임에 GPU로 넘어가서 처리된다.
- 하지만 보통 CPU가 할일이 많고, GPU는 노는 경우가 많다.
- 설상가상으로 GPU 작업이 늘어나면 CPU타임은 그보더 많이 늘어나버린다 -> Idle 타임은 더 늘어나고, 프레임 시간 제한을 맞추지 못하게 된다.
- 이는 CPU 타임에 GPU API를 호출하는 비용도 포함되기 떄문이다 -> 메탈은 이를 줄이는 것을 목표로 한다
- GPU API를 호출하는 비용을 줄이면 CPU에 여유가 생기고, 여기에 물리 엔진이나 AI등의 연산을 끼워넣을 여지도 충분히 생긴다.
- 왜 GPU 프로그래밍 연산은 무거운가? -> 메탈이 고치려는 부분
- 상태 검증 비용
- API 사용이 적합한지 검증
- API 상태를 하드웨어 상태로 인코딩해야 된다.
- 셰이더 컴파일
- 런타임에 셰이더의 기계어 코드를 컴파일 한다.
- 상태와 셰이더 간의 상호 작용도 있다.
- GPU로 작업을 전송
- 무거운 작업은 덜 자주 하자
- 작업이 일어나는 빈도를 분류하자
- 앱 빌드 시간에 일어나는 작업 : 사용자가 경험하지 않는 시간
- 컨텐츠 로딩: 가끔 일어난다.
- Draw call: 프레임 당 1000번 이상
- 기존에는 모든 작업이 Draw Call에서 일어났다. -> 이를 분산하자
- 앱 빌드 떄 셰이더를 컴파일하자
- 컨텐츠를 로딩할 때 상태를 검증하자
- GPU작업만 Draw call 할 때 시키자.
- API 컨셉
- Metal 객체
- Device: GPU
- Command Queue: command buffer의 시퀀스
- Command Buffer: GPU 하드웨어 커맨드가 담긴 객체
- Command Encoder: API 커맨드를 GPU 하드웨어 커맨드로 번역함
- State: 프레임 버퍼 설정, 블렌드, 뎁스, 샘플러 등…
- Render Pipeline state는 Descriptor에 Vertex format(descriptor), Vertex function, Fragment Function, Blend(Blend Descriptor)등을 설정한 뒤에 만들어 낸다.
- Depth Stencil State도 Descriptor를 통해서 만든다.
- Code: 셰이더들
- Resources: 텍스쳐와 데이터 버퍼(정점, 상수 등)
- 텍스처 리소스는 입력으로 받을 수도, 렌더링 패스의 일부로 고정적으로 설정할 수 있다.
- 멀티스레딩 지원: Command Queue가 thread safe하다.
- 각 스레드에서 버퍼를 만들어서 제출할 수 있따.
- command 제출 모델
- command encoder를 통해서 API 커맨드를 하드웨어 커맨드로 바꿔준다.
- 하드웨어 커맨드는 command buffer에 저장된다.
- encoder는 Render, Compute, Blit의 세종류로, 모두 하나의 command buffer 타입으로 바뀐다.
- 각 encoder는 모든 상태를 다 가지고 있기 때문에 타입을 분리하면 타입간 전환 연산이 많이들기 떄문이다.
- 앱은 가벼운 커맨드 버퍼 타입을 많이 만들어서 제출한다
- Metal은 command buffer가 실행이 끝나면 앱에 신호를 준다.
- Command Encoder는 command를 즉시 만든다.
- 상태 검증은 지연되지 않는다
- driver에 대한 직접 호출
- encoding은 병렬적으로 이루어진다.
- 앱이 이 실행 순서를 조정할 수 있따.
- 스케일업 하기 좋도록 효율적으로 구현되어 있다.
- Resource Update Model
- A7의 통합 메모리 시스템에 맞게 디자인됨
- CPU와 GPU가 같은 저장소를 공유
- 암시적 복사 없음
- 자동적인 CPU&GPU 통합 모델
- CPU와 GPU는 command buffer 실행 경계마다 쓰기가 일어나는 것을 Observing한다.
- 명시적인 CPU 캐시관리는 필요하지 않다.
- 높은 성능 -> 대신 싱크 맞추는 책임은 개발자에게.
- 리소스 종류는 2가지
- 텍스처(포맷팅된 이미지): 렌더 타겟, 텍스처 소스
- 데이터 버퍼(포맷팅되지 않은 바이트): 정점 데이터, 셰이더 상수, 아웃풋 메모리 등
- 리소스 구조(사이즈, 레벨, 포맷)은 바꿀 수 없다.
- 리소스 검증 비용을 생략하기 위해
- 컨텐츠는 바뀔 수 있다.
- 데이터 버퍼 업데이트
- 포인터를 통해서 직접 저장소에 접근
- 락은 필요없다.
- 텍스처 업데이트
- 구현체만 아는 private한 저장소에 변환되어 저장 -> 메소드로만 업데이트 가능
- Blit command Encoder를 통해서 GPU 가속된 파이프라인 업데이트
- 텍스쳐 저장소는 여러 텍스쳐 간에 공유가 가능
- 포맷 해석을 다르게 할 수 있다(사이즈는 동일)
- 데이타 버퍼와 텍스처 데이터를 공유 가능 -> 행 기반 픽셀 데이터로 취급
- 커맨드 인코더 타입
- Render: 그래픽스 렌더링 용
- 단일 렌더링 패스용 커맨드를 만든다.
- 단일 프레임 버퍼를 다룬다.
- 3D 파이프라인에서 vertex와 fragment 단계에 필요한 상태를 정의한다.
- 리소스와 상태 변화, 드로우 콜을 interleave한다(접근 속도를 향상하기 위해 엇갈려서 위치 배치를 하는 것)
- 드로우할 때 컴파일하는 경우는 없다.
- 사용 객체
- DepthStencil
- Sampler
- Render Pipeline
- 상태중에서 컴파일을 해야 되는 것들은 객체 생성후 바꿀 수 없다. 그게 아닌 경우에만 바꿀 수 있다.
- 프레임 버퍼 적재 및 저장
- 타일 기반 지연 모드 렌더러
- 명시적인 프레임버퍼 타일 캐시 적재 및 저장 연산
- 렌더 패스 시작할 때 적재, 렌더 패스 끝나고 저장
- 앱은 최적화를 위해 동작 커스텀 가능
- 적재: Don’t care, load, clear