초기화
Device 획득
Device는 Metatl의 핵심 오브젝트 → GPU 드라이버 및 하드웨어와 다이렉트로 연결
Metal의 다른 API객체들을 만드는 역할
protocol MTLDevice {
func makeCommandQueue() -> MTLCommandQueue?
func makeBuffer(...) -> MTLBuffer?
func makeTexture(...) -> MTLTexture?
func makeSamplerState(descriptor: MTLSamplerDescriptor) -> MTLSamplerState?
func makeRenderPipelineState(...) throws -> MTLRenderPipelineState
// 그 외 다수
}
// 디바이스 가져오기
let device: MTLDevice? = MTLCreateSystemDefaultDevice()
CommandQueue생성
let commandQueue: MTLCommandQueue? = device.makeCommandQueue()
리소스(Buffer&Texture) 생성
let buffer = [...]
withUnsafePointer(buffer) {
// 새로운 버퍼를 할당하고 포인터가 가리키는 내용을 복사한다.
// 복사하지 않는 버전도 있다.
device.makeBuffer(bytes: $0, // RawPointer로 암시적 캐스팅
length: buffer.count,
options: [])
}
렌더 파이프 라인 생성
let desc = MTLRenderPipelineDescriptor()
// 셰이더 설정
let library: MTLLibrary? = device.makeDefaultLibrary()
desc.vertexFunction = library?.makeFunction(name: "myVertexShader")
desc.fragmentFunction = library?.makeFunction(name: "myFragmentShader")
// 픽셀 포맷 설정. 나머지 설정값은 기본값으로 잘 맞춰진다.
desc.colorAttachments[0].pixelFormat = .bgra8Unorm
let renderPipeline = try device.makeRenderPipelineState(descriptor: desc)
뷰 생성
class MyView: UIView {
override var layerClass: AnyClass {
return CAMetalLayer.self
}
}
전체 플로우
// 1. Device 획득
guard let device = MTLCreateSystemDefaultDevice() else {
return
}
// 2. CommandQueue 생성
guard let commandQueue = device.makeCommandQueue() else {
return
}
// 3. 리소스 생성
var bytes: [UInt8] = // Raw Pointer로 넘겨야 됨.
device.makeBuffer(bytes: &bytes, length: bytes.count, options: [])
// 4. 파이프라인 생성
let desc = MTLRenderPipelineDescriptor()
// 셰이더 설정
let library = device.makeDefaultLibrary()
desc.vertexFunction = library?.makeFunction(name: "myVertexShader")
desc.fragmentFunction = library?.makeFunction(name: "myFragmentShader")
// 프레임 버퍼의 픽셀 포맷 지정
desc.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let renderPipeline = try? device.makeRenderPipelineState(descriptor: desc) else { return }
// 5. 뷰 만들기
let view = MyView() //layer가 CAMetalLayer인 뷰
struct Vertex {
float4 position;
float4 color;
};
struct VertexOut {
float4 position [[position]];
float4 color;
}
vertex VertexOut myVertexShader(
const global Vertex* vertexArray [[buffer(0)]],
unsigned int vid [[vertex_id]])
{
VSOut out;
out.position = vertexArray[vid].position;
out.color = vertexArray[vid].color
return out;
}
fragment float4 myFragmentShader(
VertexOut interpolated [[stage_in]] {
return interpolated.color;
}
// 현재 프레임의 타겟 drawalbe을 가져온다.
// 쓸 수 있는 텍스쳐가 당장 없으면 스레드가 블록될 수 있다.
let drawable = metalLayer.nextDrawable()
// 1. 커맨드 버퍼 획득
let commandBuffer = commandQueue.makeCommandBuffer()
let renderDesc = MTLRenderPassDescriptor()
renderDesc.colorAttachments[0].texture = drawable.texture
renderDesc.colorAttachments[0].loadAction = .clear
renderDesc.colorAttachments[0].clearColor = MTLClearColor()
// 2. 랜더 패스 시작
let render = commandBuffer?.makeRenderCommandEncoder(descriptor: renderDesc)
// 3. 드로잉
render?.setRenderPipelineState(renderPipeline)
render?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
render?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
render?.endEncoding()
// 4. 커맨드 버퍼에 커밋
// Core Animation에 해당 drawable 객체를 표시할지를 알려줌
// 당장은 그려진게 없으니, 커밋 이후에 다그려지면 메탈이 Core Animation에 통보
commandBuffer?.present(drawable)
// commandBuffer를 CommandQueue에 커밋
// 큐에 들어간 이후에 실제 모든 작업이 일어난다.
commandBuffer?.commit()
Uniforms and Synchronization
struct Uniforms {
float4x4 mvp_matrix;
};
vertex VSOut vertexShader(
const global Vertex* vertexArray [[ buffer(0) ]],
constant Uniforms& uniforms [[ buffer(1) ]],
unsigned int vid [[ vertex_id]]) {
VSOut out;
out.position = uniform.mvp_matrix * vertexArray[vid].position;
out.color = half4(vertexArray[vid].color);
return out;
}
// Uniform data 는 만들었다 가정
// Buffer를 만들고, 포인터로 바인딩해서, 데이터를 설정해준다..
render?.setRenderPipelineState(renderPipeline)
render?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
render?.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
render?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
let avaliableResources = DispatchSemaphore(value: 3)
// 프레임 루프
{
avaliableResources.wait()
// commandBuffer 생성
// ...
commandBuffer?.addCompletionHandler { commandBuffer in
avaliableResources.signal()
}
commandBuffer?.commit()
}
writing shaders in Metal
struct VertexOutput {
float4 pos [[ position ]];
float2 uv;
}
VertexOutput
texturedQuadVertex(const float4* vtx_data,
const float2* uv_data,
uint vid) {
VertexOutput v_out;
v_out.pos = vtx_data[vid];
v_out.uv = uv_data[vid];
return v_out;
}
#include <metal_stdlib> // metal은 C++ 표준 라이브러리 대신, GPU에 최적화된 자체 표준 라이브러리를 쓴다.
using namespace metal;
struct VertexOutput {
float4 pos [[ position ]];
float2 uv;
}
// vertex shader임을 나타내기 위한 vertex 키워드 추가
vertex VertexOutput
texturedQuadVertex(const global float4* vtx_data [[ buffer(0) ]], // buffer에서 몇번째 데이터인지를 명시
const float2* uv_data [[ buffer(1) ]],
uint vid [[ vertex_id ]]) {
VertexOutput v_out;
v_out.pos = vtx_data[vid];
v_out.uv = uv_data[vid];
return v_out;
}
// fragment shader임을 나타내기 위한 fragment키워드 추가
fragment float4
textureQuadFragment(VertexOutput frag_input [[ stage_in ]], // vertex shader의 output이 input으로 들어간다.
texture2d<float> tex [[ texture(0) ]],
sampler s [[ sampler(0)) ]]
Data types in Metal
Texture
템플릿 기반
컬러 타입(컬러 데이터가 담긴 벡터 타입)과 접근 모드(샘플링, 읽기, 쓰기)를 템플릿 인자로 받는다. → 모드에 따라 타입이 달라진다. 이는 최적화 때문
fragment FragOutput
my_fragment_shader(
texture2d<float> tA [[ texture(0) ]], // default는 sample
texture2d<half, access::write> tB [[ texture(1) ]],
depth2d<float> tc [[ texture(2) ]],
...)
{
}
Sampler
텍스쳐와 분리된 존재. → 여러 텍스쳐에 같은 샘플러를 쓸 수 있다. 혹은 여러 샘플러를 한 텍스쳐에 쓸 수도 있다.
fragment float4
texturedQuadFragment(VertexOutput frag_input [[ stage_in ]],
texture2d<float> tex [[ texture(0) ]],
sampler s [[ sampler(0]])
{
return tex.sample(s, frag_input.texcoord);
}
아니면 아예 코드상으로 정의할 수도 있다.
// 가변 템플릿
constexpr sampler s(coord::normalized,
filter::linear,
address::clamp_to_edge);
// sampler 프로퍼티를 명시하지 않았을 때의 기본 값
constexpr sampler s(address::clamp_to_zero);
buffer
Shader inputs, outputs, and matching rules
Metal Standard library