momodudu.zip
Metal 스터디 (4) - MSL 본문
Apple의 MSL Spec문서를 보는데, 일단 양이 굉장히 방대해서.. 간단한 부분들만 정리해보려고한다.
봐도봐도 헷갈리는 MSL. 간단한 예제들로 머가 어떻게 구성되는지 알아보자.
1. Metal Coordinate System
Metal의 NDC Space는 왼손 좌표계로, 아래와 같은 공간으로 구성된다. z는 [0, 1]의 범위로 구성되어 있다. 즉, 중앙값이 0.5이다.
그리고 NDC에서 나온 ouput은 rasterization을 거쳐서 뷰포트로 전환이 되는데, 원점은 좌상단인 viewport coordinate system이 완성된다.
2. Address Space
Metal의 memory는 MSL program내의 memory object들의 구조와 동작을 묘사할 수 있도록 모델링되었다. address space의 attribute는 buffer memory object가 할당되어있는곳의 어느영역에 memory가 위치해있는지를 명시해준다. 이 address space는 아래와 같은 종류가 존재한다.
- device
- constant
- thread
- threadgroup
- threadgroup_imageblock
- ray_data
MSL 쉐이더 몇개를 보다보면.. device나 constant와 같은 키워드들을 심심찮게 볼 수 있는데, 이 address space를 의미하는 키워드이다.
쉐이더 내부로 들어오는 특정 타입을 레퍼런싱하는 argument들은 반드시 address space attribute로 선언이 되어야 한다. graphics function에서는 이러한 타입들이 device 혹은 constant address spcae에 선언된다. kernal function의 경우, device, threadgroup, threadgroup_imageblock, constant에 저장된다고 한다. 여기서 말하는 graphics function은 vertex / fragment로 명시된 소위 말하는 vertex program, fragment program을 말하는것 같고, kernal function의 경우 일반 그 외의 타입들(void)로 정의되는 함수들을 의미하는것 같다.
암튼, 우리는 vertex/fragment program만 살펴볼꺼니까 device와 constant address space에 대해서만 살펴본다.
1) Device Address Space
Device Address space는 읽기/쓰기가 모두 가능한 memory pool에 할당된 buffer memory obejct를 레퍼런싱 할 때 사용된다. buffer memorry object는 pointer나 스칼라 및 벡터, 사용자가 정의한 structure등에 대한 레퍼런싱을 정의할 수 있다. Metal API단에서 buffer memory object를 할당할 때, buffer memory의 실질적인 사이즈가 결정된다.
device float4 *color;
struct Foo {
float a[3];
float b[2];
}
device Foo *foo;
위와 같은 형태로, 사용자가 정의한 structure type에 대해서도 할당된 buffer memory object를 가리킬 수 있다. texture object의 경우, 항상 device address space에 할당되기 때문에 텍스쳐 변수의 경우 device 키워드를 붙이지 않아도 된다고 한다. 대신에, direct하게 access는 불가능하기 때문에 별도의 built-in function을 사용해서 텍스쳐 object에 쓸 수 있다.
2) Constant Address Space
constant address sapce는 device memory pool에 할당된 buffer memory object들 이지만 "읽기 전용"의 object들을 레퍼런싱한다. 즉, constant 키워드로 정의된 애들은 변경을 할 수 없다. 키워드로 정의되어 있기 때문에, constant 키워드가 붙은 애들을 수정하려고하면 컴파일타임에 에러를 내준다고 한다.
말그대로 그냥 c++에서의 const 전역변수? 정도로 생각하면 쉬울것 같다. 초기화를 안해주면 컴파일에러가 나구...
이 두가지중에 어떤것을 써야할지? 에 대해서 감이 안온다면.. graphics function혹은 kernal function에서 해당 버퍼를 어떻게 접근하는지를 보면 된다고 한다. constant의 경우, 버퍼 안의 같은 Location에 접근하는 graphics function / kernal function들을 여러개 실행하는데에 최적화되어 있다고 한다. 즉 동기화가 필요없다. 예를 들어서, 조명효과를 줄 때 material이나, matrix 이런것들.. 즉 같이 써도 문제가 없는애들을 constant로 써주면 된다. 특히, 프로그램 스코프내에서만 쓰이는 애들을 constant로 선언한다고 한다.
const의 경우, 가끔씩 device앞에 키워드로 써지기도 한다.
3. Function Arguments and Variables
쉐이더에서 연산된 대부분의 input / output들, vertex혹은 fragment, 아니면 kernal function들은 argument로 전달된다. 그 대표적인 예시들은 아래와 같다.
- Device Buffer : device address space에 있는 어떤 data type에 대한 reference 혹은 pointer
- Constant Buffer : constant address space에 있는 어떤 data type에 대한 Reference 혹은 pointer
- texture object
- texture buffer object
- sampler object 등등..
device buffer 혹은 constant buffer는 graphics function에 argument로서 전달이 되는데, 이 argument는 Reserved word다. 즉, argument로 전달되는 이러한 버퍼들은 다른 버퍼들과 겹칠 수 없다.
이러한 argument들은 아래와 같은 것들을 specify하기 위해서 사용된다.
- argument의 resource location
- fixed-function과 programmable pipeline stage간의 data 주고 받기를 지원하는 built-in variable
- vertex shader -> fragment shader function으로 어떤 데이터가 보내지는지 등.
1) Locating Buffer, Texture, Sample Argument
각 argument들에 대해서, attribute를 이용해서 각 buffer, texture, sample등의 buffer location을 명시해줄 수 있다. Metal에서는 아래와 같은 argument type들에 이러한 attribute를 이용한다.
- Device and Constant Buffer : [[ buffer(index) ]]
- texture : [[ texture(index) ]]
- sampler : [[ sampler(index) ]]
index값은 unsgined int이다.
여기서 index값은 metal API단에서 결정이 되는데, encoder.setVertexBuffer(buffer, offset: index:)를 설정할 때, 제일 마지막 파라미터인 index:가 위의 Index에 상응한다.
struct VertexIn {
packed_float3 position;
packed_float4 color;
};
struct VertexOut {
float4 position [[position]];
float4 color;
};
vertex VertexOut vertex_main(
const device VertexIn* vertex_array [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]])
{
VertexIn vertexIn = vertex_array[vid]; // get the current vertex from the array
VertexOut vertexOut; // vertex out으로 내보낼 데이터를 만든다.
vertexOut.position = float4(vertexIn.position, 1.0); // position, 1.0으로 된 float4와
return vertexOut;
}
위는 간단한 vertex shader예제이다. vertex function인 vertex_main의 argument중 첫번째를 보면, vertex_array를 buffer(0)으로 argument를 지정해놓았다. 두번째 argument인 vertex_id는 built-in variable인데, vertex_main이 버텍스 단위로 수행되는 program이므로 vertex_id는 " 그 수행되는 해당 버텍스의 identifier " 이다.
이 방법들은 함수 argument에 선언된 키워드들이나 포인터를 보면 알 수 있는데.. 배열의 형태로 전달된다. ( 실제로는 포인터지만 )
그래서 vid를 이용해서 buffer location내에 실제 per-vertex에 접근을 할 수 있다. 그래서 보면.. [[ buffer(0) ]]에 해당하는 argument가 들어왔을 때, 현재 per vertex identifier인 vertex_id를 이용해서 vid를 얻어온다. 그래서 vertex_array에서 해당하는 현재 버텍스를 얻어온게 vertexIn이다.
2) Attribute to Locate per-vertex inputs
위에 방법은 vertex와 전달된 instance ID를 통해서 전달된 buffer에 접근하는 방법이었다. vertex shader에서 또 다르게 per-vertex에 접근하는 방법이 있는데, [[stage_in]] attribute와 vertex input을 per-vertex input argument로 전달해서 접근하는 방법이 있다.
[[stage_in]] attribute와 함께 선언되어 전달하는 per-vertex input은 각 vertex attribute의 location을 [[attribute(index)]]를 써서 정의해주어야 한다. 즉, stage_in를 쓰려면 attribute(index)도 같이 써주어야 한다. 특히 이 attribute를 쓰려면 Metal API단에서 pipelineStateDescriptor의 해당 attribute의 데이터 포맷, 스트라이드등을 vertexDescriptor를 이용해서 정의해주어야 한다.
struct VertexIn {
float3 position [[attribute(0)]];
float4 color [[attribute(1)]];
};
vertex VertexOut vertex_main(
VertexIn in [[ stage_in ]],
unsigned int vid [[ vertex_id ]])
{
VertexOut vertexOut; // vertex out으로 내보낼 데이터를 만든다.
vertexOut.position = float4(in.position, 1.0);
vertexOut.color = float4(in.color, 1.0);
return vertexOut;
}
attribute(index)를 쓰면 똑같은 쉐이더라도 약간 형태가 달라진다. vs function으로 들어가는 vertex array의 형태가 버퍼형태, 즉 포인터 형태가 아닌 attribute자체에 stage_in으로 인덱싱 된 값을 가져올 수 있다. 쉐이더 펑션 내부에서 들어오는 vertex를 어떻게 쓰는지만 보아도 전달되는 형태가 두가지가 어떻게 다른지 알 수 있다.
3) Attributes for built-in variables
built-in input/output variable은 vertex fucntion / fragment function와 graphics pipeline stage내의 fixed function간의 data 전달을 위해서 사용된다.
- Vertex Function의 input attribute의 대표적인 예 => [[vertex_id]], per-vertex identifier로 쓰인다.
- Vertex Function의 ouput attribute의 대표적인 예 => [[position]], 버텍스 function에서 변환을 거친 vertex position을 명시한다. 주로 VertexOut 구조체등에 position을 position[[ position ]]의 형태로 attribute를 명시한다.
그 외에도 fragment function에서도 사용되는 빌트인 변수들이 있지만, 간단하게 알아보는건 일단 여기까지..
두개중 이럴땐 이런거 써라! 라는 규약은 없는듯하다.
다만, metal로 보낼 vertex들이 interleaved인지, non-interleaved data인지 그 때에 따라서 적절하게 사용하면 될 것 같다.
예를 들어서, Vertex하나가 (x,y,z,r,g,b,a)형태로 들어가서
v1 - (x1,y1,z1,r1,g1,b1,a1)
v2 - (x2,y2,z2,r2,g2,b2,a2)의 형태로 interleaved 형태라면 첫번째 방법, 즉 [[ buffer(index) ]]의 형태로 사용하는게 편리하다.
반면에 Non-interleaved data,
v1 - (x1,y1,z1) / v2 - (x2,y2,z2)
c1 - (r1,g1,b1,a1) / c2 - (r2,b2,g2,a2)
이런 형태로 vertex position에 해당하는 값들, 위의 경우 컬러값들이 따로 버퍼에 적재가 되어있다면 attribute형태를 사용해서 쓰는것이 편할듯 하다. 물론 1의 형태로 interleaved data여도 vertex descriptor에서 offset을 잘 지정해주면 attribute의 형태로 사용할 수 있지만 offset을 계산하는게 귀찮으니까..
'ios > Metal' 카테고리의 다른 글
Metal 스터디(6) - matrix 적용해보기 ( Model Transformation ) (0) | 2022.02.10 |
---|---|
Metal 스터디(5) - MSL에서 packed_float 과 non-packed_float의 차이 (0) | 2022.02.09 |
Metal 스터디 (3) - Rendering Pipeline (0) | 2021.11.26 |
Metal 스터디(2) - Vertex Descriptions / Metal Coordinates System (0) | 2021.11.24 |
Metal 스터디(1) - 시작하기 (0) | 2021.11.23 |