1. Metal Shading Language简介
Metal着色语言是用来编写3D图形渲染逻辑
和并行计算核心逻辑
的一门编程语言,底层使用Clang和LLVM进行编译处理。
Metal语言基于C++ 11.0 语言设计,我们主要用来编写在GPU上执行的图像渲染逻辑代码
以及通用并行计算逻辑代码
。当我们使用Metal框架来完成APP的实现时则需要使用Metal编程语言。
2. Metal 使用常识
2.1 Metal 与 C++ 11.0
Metal Restrictions 限制 (如下的C++11.0的特性在Metal 着色语言中是不支持的
):
- Lambda表达式
- 递归函数调⽤
- 动态转换操作符
- 类型识别
- 对象创建(
new
)和释放(delloc
)操作符 - 操作符
noexcept
-
goto
跳转 - 虚函数修饰符-
- 派⽣类
- 异常处理
C++的标准库不可以在Metal 着色语言中使用。
2.2 Metal 语言中对于指针使用的限制
- Metal图形和并行计算函数用到的入参,如果是指针必须使用地址空间修饰符 (device ,threadgroup ,constant)
- 不支持函数指针
- Metal函数名不能命名为Main函数
2.3 Metal 与OpenGL ES
- 在OpenGL ES中,图片的纹理坐标原点是基于左下角的,而在Metal中,苹果为了统一,将图片纹理的坐标原点设在了左上角。
- 在OpenGL ES中,需要我们手动去编译顶点着色器和片元着色器,而在Metal中,这一切是由苹果系统为我们做的。
3.0 Metal数据类型
类型 | 描述 |
---|---|
bool | 布尔类型,取值范围true,false;true可以拓展为整数常量1,false可以拓展为整数常量0 |
char | 有符号8-bit整数 |
unsigned char (uchar) | 无符号8-bit整数 |
short | 有符号16-bit整数 |
unsigned short (ushort) | 无符号16-bit整数 |
int | 有符号32-bit整数 |
unsigned int (uint) | 无符号32-bit整数 |
half | 一个16-bit浮点数 |
float | 一个32-bit浮点数 |
size-t | 64-bit无符号整数,表示sizeof操作符的结果 |
ptrdiff_t | 64-bit有符号整数,表示2个指针的差 |
void | 表示一个空的值集合 |
3.1 Metal 向量和矩阵数据类型
3.1.1 向量
- booln
- charn
- shortn
- intn
- ucharn
- ushortn
- uintn
- halfn
- floatn
向量中的n,指的是维度,当n=1时直接省略,n最大是4。
bool2 A= {1,2};
float4 pos = float4(1.0,2.0,3.0,4.0);
float x = pos[0];
float y = pos[1];
float4 VB;
for(int i = 0; i < 4 ; i++)
VB[i] = pos[i] * 2.0f;
//通过向量字母来获取元素
int4 test = int4(0,1,2,3);
int a = test.x;
int b = test.y;
int c = test.z;
int d = test.w;
int e = test.r;
int f = test.g;
int g = test.b;
int h = test.a;
float4 c;
c.xyzw = float4(1.0f,2.0f,3.0f,4.0f);
c.z = 1.0f;
c.xy = float2(3.0f,4.0f);
c.xyz = float3(3.0f,4.0f,5.0f);
float4 pos = float4(1.0f,2.0f,3.0f,4.0f);
float4 swiz = pos.wxyz; //swiz = (4.0,1.0,2.0,3.0);
float4 dup = pos.xxyy; //dup = (1.0f,1.0f,2.0f,2.0f);
//pos = (5.0f,2.0,3.0,6.0)
pos.xw = float2(5.0f,6.0f);
//pos = (8.0f,2.0f,3.0f,7.0f)
pos.wx = float2(7.0f,8.0f);
//pos = (3.0f,5.0f,9.0f,7.0f);
pos.xyz = float3(3.0f,5.0f,9.0f);
float2 pos;
pos.x = 1.0f; //合法
pos.z = 1.0f; //非法
float3 pos2;
pos2.z = 1.0f; //合法
pos2.w = 1.0f; //非法
//非法,x出现2次
pos.xx = float2(3.0,4.0f);
//不合法-使用混合限定符
pos.xy = float4(1.0f,2.0,3.0,4.0);
float4 pos4 = float4(1.0f,2.0f,3.0f,4.0f);
pos4.x = 1.0f;
pos4.y = 2.0f;
//非法,.rgba与.xyzw 混合使用
pos4.xg = float2(2.0f,3.0f);
////非法,.rgba与.xyzw 混合使用
float3 coord = pos4.ryz;
float4 pos5 = float4(1.0f,2.0f,3.0f,4.0f);
//非法,使用指针来指向向量/分量
my_func(&pos5.xy);
3.1.2 矩阵
矩阵支持如下类型:
- halfnxm
- floatnxm
nxm分别指的是矩阵的⾏数和列数,最大支持4行4列
例:
float4x4 m;
//将第二排的值设置为0
m[1] = float4(2.0f);
//设置第一行/第一列为1.0f
m[0][0] = 1.0f;
//设置第三行第四列的元素为3.0f
m[2][3] = 3.0f;
float4类型向量的所有可能构造方式
float4(float x);
float4(float x,float y,float z,float w);
float4(float2 a,float2 b);
float4(float2 a,float b,float c);
float4(float a,float2 b,float c);
float4(float a,float b,float2 c);
float4(float3 a,float b);
float4(float a,float3 b);
float4(float4 x);
float3类型向量的所有可能的构造的方式
float3(float x);
float3(float x,float y,float z);
float3(float a,float2 b);
float3(float2 a,float b);
float3(float3 x);
float2类型向量的所有可能的构造方式
float2(float x);
float2(float x,float y);
float2(float2 x);
4.纹理Texture类型
纹理类型是一个句柄,它指向一个一维/二维/三维纹理数据。
枚举值:定义了访问权限:
enum class access (sample,read,write);
sample:纹理对象可以被采样;
read:不使用采样器,一个图形渲染函数或者一个并行计算函数可以读取纹理对象;
write:一个图形渲染函数或者一个并行计算函数可以向纹理对象写入数据。
texture1d
texture2d
texture3d
T:数据类型,设定了从纹理中读取或者是向纹理中写入时的颜色类型,T可以是half,float,short,int等。
举例
void foo (texture2d imgA [[ texture(0) ]] ,texture2d imgB [[ texture(1) ]], texture2d imgC [[ texture(2) ]])
{
...
}
5. 采样器 Samples
采样器类型决定了如何对一个纹理进行采样操作。在Metal框架中有一个采样器对象MTLSamplerState,这个对象作为图形渲染着色器函数参数或者是并行计算函数的传递参数,也就是在sample中我们设置纹理的采样方式(环绕方式、过滤方式)。在Metal程序中初始化的采样器必须使用constexpr
修饰符修饰。
- Metal支持的采样器状态和默认值
枚举名称(Enum Name) | 有效值(Valid Value) | 描述 |
---|---|---|
coord | normalized | 从纹理中采样时,纹理坐标是否需要归一化 |
address | clamp_to_edge,clamp_to_zero,mirrored_repeat,repeat | 设置所有纹理坐标的寻址模式 |
s_address,t_address,r_address | clamp_to_edge,clamp_to_zero,mirrored_repeat,repeat | 设置某个纹理坐标的寻址模式 |
filter | nearest,linear | 设置纹理采样的放大和缩小的过滤模式 |
mag_filter | nearest,linear | 设置纹理采样的放大过滤模式 |
min_filter | nearest,linear | 设置纹理采样的缩小过滤模式 |
mip_filter | none,nearest,linear | 设置纹理采样的minmap过滤模式,如果是none,那么只有一个层纹理能生效 |
compare_func | none,less,less_qual,greater,greater_equal,equal,not_equal | 为使用r纹理坐标做shaw map,设置比较测试逻辑,这个状态值的设置只可以在Metal着色语言程序中完成 |
从纹理中采样时,纹理坐标是否需要归一化
enum class coord {noramlized,pixel };
纹理采样过滤方式,放大/缩小过滤模式
enum class filter {nearest, linear};
设置纹理采样缩小过滤模式
enum class min_filter {nearest, linear};
设置纹理采样放大过滤模式
enum class mag_filter {nearest, linear};
设置s,t,r纹理坐标的寻址模式
enum class s_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat };
enum class t_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat };
enum class r_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat };
设置所有纹理坐标的寻址模式
enum class address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat };
例:
constexpr samper s(coord::pixel,address::clamp_to_zero,filter::linear);
6. 函数修饰符
- kernel ,表示该函数是一个数据并行计算着色函数,它可以被分配在一维/二维/三维线程组中去执行。
注意:使用kernel修饰的函数,气返回值类型必须是void类型。
kernel void test(...)
{
......
}
- vertex,表示该函数是一个顶点着色函数,它将为顶点数据流中的
每个顶点数据执行一次
然后为每个顶点生成数据
输出到绘制管线。 - fragment,表示该函数是一个片元着色函数,它将为片元数据流中的
每个片元
和其关联执行一次然后将每个片元
生成的颜色数据输出到绘制管线中。
只有图形函数才可以被vertex和fragment修饰。对于图形修饰函数,可以根据返回值判断其是为顶点做计算还是为像素做计算。注意:一个被函数修饰符修饰的函数不能调用另一个函数修饰符修饰的函数。
kernel void test1(...)
{
}
vertex float4 test2(...)
{
test1(...); // 这个是错误的调用
}
7. 地址空间修饰符
地址空间修饰符,是Metal着色语言用来给参数变量或者函数变量分配内存使用的。所有的着色函数(kernel,vertex,fragment)的参数,如果是指针或者是引用,都必须带有地址空间修饰符。
- device(设备地址空间)
在设备地址空间(device)指向设备内存池(这里指的是显存
)分配出来的缓存对象,既是可读也是可写的。
device float4 *color;
struct test{
float a[2];
int b[1];
}
device test *info;
注意:纹理对象总是在设备地址空间分配内存,device地址空间修饰符不必出现在纹理类型定义中,一个纹理对象的内容无法直接访问,可使用Metal自身的内建函数。
- threadgroup(线程组地址空间)
线程组地址空间用于为并行计算着色函数分配内存变量,这些变量被一个线程组的所有线程共享。
在线程组地址空间分配的变量不能被用于图形绘制着色函数。
在并行计算周瑟函数中,在线程组地址空间分配的变量为一共线程组共同使用,生命周期和线程组相同。
kernel void func(threadgroup float *a[[threadgroup(0)]])
{
....
}
- constant(常量地址空间)
常量地址空间指向的缓存对象也是从设备内存池分配存储,但是它是只读的。
在程序域的变量的生命周期和程序一样,在程序中的并行计算着色函数或者图形绘制着色函数使用,但是constant的值是不会变的。
注意:常量地址空间的指针或是引用可以作为函数的参数。向声明为常量的变量赋值会编译失败,声明常量不赋值也会编译失败。
constant float samples[] = {1.0f,2.0f,3.0f,4.0f};
samples[4] = {3,2,3,2}; //编译失败,因为constant是只读的
constant float a; // 编译失败,因为没有赋值
- thread(线程地址空间)
thread地址空间指向每个线程准备的地址空间,这个线程的地址空间定义的变量在其他线程不可见,在图形绘制着色函数或者并行计算着色函数中可使用thread声明。
kernel void func(...)
{
float x;
thread float p = &x;
...
}
对于图形着色函数,其指针或是引用类型的参数必须定义为device或者是constant地址空间,对于并行计算着色函数,其指针或是引用类型的参数必须定义为device或者是constant或者是threadgroup地址空间
8. 函数参数与变量
图形绘制或者并行计算着色器函数的输入输出都是通过参数传递,除了常量地址空间变量和程序域定义的采样器除外。
- device buffer - 设备缓存,一个指向设备地址空间的任意数据类型的指针或者引用;
- constant buffer - 常量缓存区,一个指向常量地址空间的任意数据类型的指针或者引用;
- texture - 纹理对象;
- sampler - 采样器对象;
- threadgroup - 在线程组中供各线程共享的缓存。
注意:被着色器函数的缓存(device和constant)不能重名。
对于每个着色器函数来说,一个修饰符是必须指定的,他用来设定一个缓存,纹理,采样器的位置。
- device buffer/constant buffer → [[buffer(index)]]
- texture → [[texture(index)]]
- sampler → [[sampler(index)]]
- threadgroup → [[threadgroup(index)]]
index是一个unsigned integer类型的值,它表示了一个缓存、纹理、采样器的位置。
kernel void add_vectros(
const device float4 *inA [[buffer(0)]],
const device float4 *inB [[buffer(1)]],
device float4 *out [[buffer(2)]]
uint id[[thread_position_in_grid]])
{
out[id] = inA[id] + inB[id];
}
thread_position_in_grid:用于表示当前节点在多线程网格中的位置;
内建变量属性修饰符
- [[vertex_id]]: 顶点id标识符
- [[position]]: 顶点信息(float4)
表示片元的窗口相对坐标
- [[point_size]]: 点的大小(float)
- [[color(m)]]:颜色,m编译前必须要确定
struct myFragmentOutput{
float4 cl_f[[color(0)]];
float4 cl_i[[color(1)]];
float4 cl_u[[color(2)]];
};
fragment myFragmentOutput shader()
{
myFragmentOutput f;
f.cl_f = ...;
...
return f;
}
- [[stage_in]]:片元着色函数使用的单个片元输入数据是由顶点着色函数输出然后经过光栅化生成的。
顶点和片元着色函数都是只能有一个参数被声明为使用“stage_in”修饰符,对于一个使用了stage_in修饰符的自定义的结构体,期结构体内部成员可以是整形、浮点型标量、浮点型向量。
属性修饰符目的:
- 参数表示资源如何定位? 可以理解为端口;
- 在固定管线和可编程管线进行内建变量的传递;
- 将数据沿着渲染管线从顶点函数传递片元函数。