초기화
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
vertex VertexOutput
my_vertex(const global float3* position_data [[ buffer(0) ]],
const global float3* normal_data [[ buffer(1) ]],
constant TransformMatrices& matrices [[ buffer(2) ]],
uint vid [[ vertex_id ]])
{
VertexOut out;
float3 n_d = normal_data[vid];
float3 transformed_normal = matrices.normal_matrix * n_d;
float4 p_d = float4([position_data[vid], 1.0f);
out.position = matrices.modelview_projection_matrix * p_d;
float4 eye_vector = matrices.modelview_matrix * p_d;
...
return out;
}
Shader inputs, outputs, and matching rules
Per-Vertex input
shader가 vertex data의 레이아웃을 알 수 있는 경우: vertex buffer를 global로 넘기고 vertex ID와 instance ID로 참조
vertex VertexOutput
my_vertex_shader(vertexInputA* inputA [[ buffer(0) ]],
vertexInputB* inputB [[ buffer(1) ]],
uint vid [[ vertex_id ]],
unit instid [[instance_id]])
{
float a = inputA[vid].a;
half4 b = inputB[instid].b;
// ...
}
shader와 vertex data 타입을 디커플링시키고 싶은 경우
OpenGL의 vertex Array API와 매칭됨
vertext Descriptor를 만들어서, 어떤 버퍼인지, 버퍼에서 어느 우치인지, 포맷이 무엇인지등을 지정한다. → 1개 이상의 버퍼를 사용 가능
struct VertexInput {
float4 position [[ attribute(0) ]];
float3 normal [[ attribute(1) ]];
half4 color [[ attribute(2) ]];
half2 texcoord [[ attribute[3 ]];
};
vertex VertexOutput
my_vertex_shader(VertexInput v_in [[ stage_in ]], ...
let vertexDesc = MTLVertexDescriptor()
let positionDesc = MTLVertexAttributeDescriptor()
positionDesc.bufferIndex = 0
positionDesc.offset = 0
positionDesc.format = .float3
vertexDesc.attributes[0] = positionDesc
let normalDesc = MTLVertexAttributeDescriptor()
normalDesc.bufferIndex = 0
normalDesc.offset = 12
normalDesc.format = .float3
vertexDesc.attributes[1] = normalDesc
let colorDesc = MTLVertexAttributeDescriptor()
colorDesc.bufferIndex = 0
colorDesc.offset = 24
colorDesc.format = .uchar4Normalized
vertexDesc.attributes[2] = normalDesc
let textureCoordDesc = MTLVertexAttributeDescriptor()
textureCoordDesc.bufferIndex = 0
textureCoordDesc.offset = 28
textureCoordDesc.format = .ushort2Normalized
vertexDesc.attributes[3] = normalDesc
let strideDesc = MTLVertexBufferLayoutDescriptor()
strideDesc.stride = 32
vertexDesc.layouts[0] = strideDesc // 이 인덱스는 버퍼 인덱스를 의미
pipelineDescriptor.vertexDescriptor = vertexDesc
Per-Vertex Outputs