简介
-
Metal
着色器语言是用来编写3D图形渲染逻辑
、并行Metal计算核心逻辑
的一门编程语言,当你使用Metal
框架来完成APP
的实现时则需要使用Metal
编程语言。 -
Metal
语言使用Clang 和LLVM
进行编译处理,编译器对于在GPU
上的代码执行效率有更好的控制 -
Metal
基于C++ 11.0
语言设计的,在C++
基础上多了一些扩展和限制,主要用来编写在GPU
上执行的图像渲染逻辑代码以及通用并行计算逻辑代码 -
Metal
像素坐标系统:Metal
中纹理 或者 帧缓存区attachment
的像素使用的坐标系统的原点是左上角
限制
- Metal中不支持C++11.0的特性:
-
Lambda
表达式; - 递归函数调用
- 动态转换操作符
- 类型识别
- 对象
创建new和销毁delete操作符
- 操作符
noexcept
goto跳转
- 变量存储修饰符
register 和thread_local
- 虚函数修饰符
- 派生类
- 异常处理
- C++标准库在Metal语言中也不可使用
-
- Metal语言对于指针使用的限制
-
Metal
图形和并行计算函数用到的入参如果是指针使用地址空间修饰符 (device ,threadgroup ,constant)
不⽀持函数指针; - 函数名不能出现main
-
数据类型
基本数据类型主要有:标量、向量、矩阵
标量
类型 | 描述 |
---|---|
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 | 表示一个空的值集合 |
常用: bool、int、uint 、half
unsigned char
可以简写为uchar
unsigned short
可以简写为ushort
unsigned int
可以简写为 uint
half
相当于OC中的float
,float
相当于OC中的double
size_t
用来表示内存空间, 相当于OC中 sizeof
bool a = true;
char b = 5;
int d = 15;
//表示内存空间
size_t c = 1;
ptrdiff_t f = 2;
向量
支持类型:booln、charn、shortn、intn、ucharn、ushortn、uintn、halfn、floatn,n 表示向量的维度,最多不超过4维
//直接赋值初始化
bool2 A= {1,2};
//通过内建函数float4初始化
float4 pos = float4(1.0,2.0,3.0,4.0);
//通过下标从向量中获取某个值
float x = pos[0];
float y = pos[1];
//通过for循环对一个向量进行运算
float4 VB;
for(int i = 0; i < 4 ; I++)
{
VB[i] = pos[i] * 2.0f;
}
向量的访问规则
- 通过
向量字母
获取元素: 向量中的向量字母仅有2种,分别为xyzw、rgba
int4 test = int4(0,1,2,3);
int a = test.x; //获取的向量元素0
int b = test.y; //获取的向量元素1
int c = test.z; //获取的向量元素2
int d = test.w; //获取的向量元素3
int e = test.r; //获取的向量元素0
int f = test.g; //获取的向量元素1
int g = test.b; //获取的向量元素2
int h = test.a; //获取的向量元素3
- 多个分量同时访问
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);
- 多分量访问可以乱序/重复
-
赋值
时分量不可重复
,取值
时分量可重复
-
右边
是取值和左边
赋值都合法 -
xyzw
与rgba
不能混合使用
-
注意:GLSL中向量不能乱序访问,只是和Metal中的向量相似,并不是等价。
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);
//可以仅对 xw / wx 修改
//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);
//可以仅对 xyz 进行修改
//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; //非法,pos是二维向量,没有z这个索引
float3 pos2;
pos2.z = 1.0f; //合法
pos2.w = 1.0f; //非法
// 赋值 时 分量不可重复,取值 时 分量可重复
//非法,x出现2次
pos.xx = float2(3.0,4.0f);
pos.xy = swiz.xx;
//向量中xyzw与rgba两组分量不能混合使用
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;
矩阵
矩阵支持类型:halfnxm、floatnxm
,其中 nxm
表示矩阵的行数和列数
,最多4行4列
。
普通的矩阵其本质就是一个数组
。
float4x4 m;
//将第二行的所有值都设置为2.0
m[1] = float4(2.0f);
//设置第一行/第一列为1.0f
m[0][0] = 1.0f;
//设置第三行第四列的元素为3.0f
m[2][3] = 3.0f;
float4
类型向量的构造方式
-
1个float
构成,表示一行都是这个值
//1个一维向量,表示一行都是x
float4(float x);/
-
4个float
构成
//4个一维向量 --> 4维向量
float4(float x,float y,float z,float w);
-
2个float2
构成
//2个二维向量 --> 4维向量
float4(float2 a,float2 b);
-
1个float2
+2个float
构成(顺序可以任意组合)
//1个二维向量+2个一维向量 --> 4维向量
float4(float2 a,float b,float c);
float4(float a,float2 b,float c);
float4(float a,float b,float2 c);
-
1个float2
+1个float
//1个三维向量+1个一维向量 --> 4维向量
float4(float3 a,float b);
float4(float a,float3 b);
1个float4
//1个四维向量 --> 4维向量
float4(float4 x);
float3
类型向量的构造方式
-
1个float
构成,表示一行都是这个值 3个float
-
1个float
+1个float2
(顺序可以任意组合) 1个float2
//float3类型向量的所有可能的构造的方式
//1个一维向量
float3(float x);
//3个一维向量
float3(float x,float y,float z);
//1个一维向量 + 1个二维向量
float3(float a,float2 b);
//1个二维向量 + 1个一维向量
float3(float2 a,float b);
//1个三维向量
float3(float3 x);
float2
类型向量的构造方式
-
1个float
构成,表示一行都是这个值 2个float
1个float2
//float2类型向量的所有可能的构造方式
//1个一维向量
float2(float x);
//2个一维向量
float2(float x,float y);
//1个二维向量
float2(float2 x);
纹理Textures
纹理类型是一个句柄,指向一维/二维/三维纹理数据,在一个函数中描述纹理对象的类型。
而纹理数据对应一个纹理的某个level的mipmap的全部或者一部分。
纹理的访问权限
access
枚举值由Metal
定义,定义了纹理的访问权利 enum class access {sample, read, write}
;,有以下3种访问权利
-
sample
: 纹理对象可以被采样. 采样⼀维这是使⽤或不使⽤采样器从纹理中读取数据; -
read
: 不使⽤采样器, ⼀个图形渲染函数
或者⼀个并⾏计算函数
可以读取纹理对象; -
write
: ⼀个图形渲染函数
或者⼀个并⾏计算函数
可以向纹理对象写⼊数据;
当没写access
时,默认的access 就是 sample
定义纹理类型
描述一个纹理对象/类型,有以下三种方式,分别对应一维/二维/三维,
texture1d
texture2d
texture3d
-
T
代表泛型
,设定了从纹理中读取数据
或是写入时的颜色类型
,如half、float、short、int
等 -
access
表示纹理访问权限
,当access
没写时,默认是sample
//类型 变量 修饰符
/*
类型
* texture2d,读取的数据类型:float,无:access,默认:sample
* texture2d,读取的数据类型:float,读取的方式:read
* texture2d,读取的数据类型:float,读取的方式:write
变量名
* imgA
* imgB
* imgC
修饰符
* [[texture(0)]] 对应纹理0
* [[texture(1)]] 对应纹理1
* [[texture(2)]] 对应纹理2
*/
void foo (texture2d imgA[[texture(0)]],
texture2d imgB[[texture(1)]],
texture2d imgC[[texture(2)]])
{
}
采样器Samplers
采取器类型决定了如何对⼀个纹理进⾏采样操作. 在Metal 框架中有⼀个对应着⾊器语⾔的采样器的对象MTLSamplerState
这个对象作为图形渲染着⾊器函数参数
或是并⾏计算函数的参数传递
。
Metal支持的采样器和默认值
-
coord
:从纹理中采样时,纹理坐标是否需要归一化
enum class coord { normalized, pixel };
-
filter
:纹理采样过滤方式,放大/缩小过滤方式
enum class filter { nearest, linear };
-
min_filter
:设置纹理采样的缩小过滤方式
enum class min_filter { nearest, linear };
-
mag_filter
:设置纹理采样的放大过滤方式
enum class mag_filter { nearest, linear };
-
mip_filter
:设置纹理采样的mipMap过滤模式
, 如果为none
只有一层纹理生效
enum class mip_filter { none, nearest, linear };
-
s_address、t_address、r_address
:设置纹理s、t、r
坐标(对应纹理坐标的x、y、z
)的寻址方式
//s坐标
enum class s_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
//t坐标
enum class t_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
//r坐标
enum class r_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
-
address
:设置所有纹理坐标的寻址方式
enum class address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
注意:在Metal
程序中初始化的采样器必须使⽤ constexpr
修饰符声明
/*
* constexpr:修饰符(必须写)
* sampler:类型
* s:采样器变量名称
参数
* coord: 是否需要归一化,不需要归一化,用的是像素pixel
* address: 地址环绕方式
* filter: 过滤方式
*/
constexpr sampler s(coord::pixel, address::clamp_to_zero, filter::linear);
constexpr sampler a(coord::normalized);
constexpr sampler b(address::repeat);
函数修饰符
Metal
有以下3种函数修饰符:
-
kernel
, 表示该函数是⼀个数据并⾏计算着⾊函数. 它可以被分配在⼀维/⼆维/三维线程组中去执⾏ -
vertex
, 表示该函数是⼀个顶点着⾊函数
, 它将为顶点数据流
中的每个顶点数据执⾏⼀次
然后为每个顶点⽣成数据输出
到绘制管线 -
fragment
,表示该函数是⼀个⽚元着⾊函数
, 它将为⽚元数据流
中的每个⽚元和其关联执⾏⼀次
然后将每个⽚元⽣成的颜⾊数据输出
到绘制管线中
注意
- 使用
kernel
修饰的函数,其返回值类型必须
是void
类型 - ⼀个被
函数修饰符
修饰的函数
不能在调⽤其他也被函数修饰符
修饰的函数
,这样会导致编译失败。 - 被函数修饰符修饰过的函数,
只允许
在客户端
对其进行操作.不允许
被普通的函数
调用 - 并
不是
所有的函数都需函数修饰符
修饰,可以定义不带任何修饰符
的普通函数。
//并行计算函数(kernel)
kernel void CCTestKernelFunctionA(int a,int b)
{
/*
* 注意:
* 1. 使用kernel 修饰的函数返回值必须是void 类型
* 2. 一个被函数修饰符修饰过的函数,不允许在调用其他的被函数修饰过的函数. 非法
* 3. 被函数修饰符修饰过的函数,只允许在客户端对其进行操作. 不允许被普通的函数调用.
*/
//不可以的!
//一个被函数修饰符修饰过的函数,不允许在调用其他的被函数修饰过的函数. 非法
CCTestKernelFunctionB(1,2);//非法,错误调用!!!
CCTestVertexFunctionB(1,2);//非法,错误调用!!!
//可以! 你可以调用普通函数.而且在Metal 不仅仅只有这3种被修饰过的函数.普通函数也可以存在
CCTest();
}
//并行计算函数
kernel void CCTestKernelFunctionB(int a,int b)
{
.....
}
//顶点函数
vertex int CCTestVertexFunctionB(int a,int b)
{
.....
}
//片元函数
fragment int CCTestVertexFunctionB(int a,int b)
{
.....
}
//普通函数
void CCTest()
{
.....
}
变量、参数的地址空间修饰符
Metal
着⾊器语⾔使⽤地址空间修饰符
来表示⼀个函数变量
或者参数变量
被分配于那⼀⽚内存区域
。
地址空间修饰符
-
device
:设备地址空间 -
threadgroup
:线程组地址空间 -
constant
:常量地址空间 -
thread
: 线程地址空间
注意:
- 所有的
着色函数(vertex、fragment、kernel)
的参数,指针
或是引用类型
的参数,都必须
带有地址空间修饰符号
。 - 对于
图形着⾊器函数
, 其指针
或是引⽤类型的参数必须
定义为device
或是constant
地址空间。 - 对于
并行计算函数
(kernel修饰的函数
),其指针
或是引用类型
的参数必须
定义为device、threadgroup、constant
/*
注意:
* 1. 所有被(kernel,vertex,fragment)所修饰的参数变量,如果其类型是指针/引用都必须带有地址空间修饰符.
* 2. 被fragment修饰的片元函数, 指针/引用必须被device/constant/threadgroup
*/
//变量/参数地址空间修饰符
void CCTestFouncitionE(device int *g_data,
threadgroup int *l_data,
constant float *c_data
)
{
//...
}
设备空间地址修饰符device
- 设备地址空间(Device) 指向
设备内存池(显存)中分配出来的缓存对象
, 它可读
也是可写
; ⼀个缓存对象可 以被声明成⼀个标量、向量或是⽤户⾃定义结构体
的指针或是引⽤
- device放在变量类型之前
// 设备地址空间: device 用来修饰指针.引用
//1.修饰指针变量
device float4 *color;
struct CCStruct{
float a[3];
int b[2];
};
//2.修饰结构体类的指针变量
device CCStruct *my_CS;
注意:
- device 除了可以修饰
图形着色器函数 / 并行计算函数
参数,还可以修饰指针变量
和结构体指针变量
- 纹理对象总是在
设备地址空间分配内存
,即GPU
分配内存 -
device地址空间修饰符
不必出现在纹理类型
定义中 - 一个纹理对象的内容
无法直接访问
,Metal提供读写
纹理的内建函数
,通过内建函数访问
纹理对象
线程组地址空间修饰符threadgroup
- 线程组地址空间用于为
并行计算着色器函数分配内存变量
,这些变量
被一个线程组的所有线程共享
,在线程组地址空间
分配的变量
不能用于图形绘制着色函数
(即顶点/ 片元着色函数),即在图形绘制着色函数
中不能使用线程组
- 在并行计算着色函数中,在
线程组地址空间分配
的变量
为一个线程组使用,生命周期和线程组相同
/*
* 1. threadgroup 被并行计算计算分配内存变量, 这些变量被一个线程组的所有线程共享. 在线程组分配变量不能被用于图像绘制.
* 2. thread 指向每个线程准备的地址空间. 在其他线程是不可见切不可用的
*/
kernel void CCTestFouncitionF(threadgroup float *a)
{
//在线程组地址空间分配一个浮点类型变量x
threadgroup float x;
//在线程组地址空间分配一个10个浮点类型数的数组y;
threadgroup float y[10];
}
线程地址空间修饰符thread
-
线程地址空间
指向每个线程准备的地址空间
,也是在GPU
中,该线程的地址空间
定义的变量
在其他线程
不可见(变量不共享) - 在
图形绘制着色函数
或者并行计算着色函数
中声明的变量,在线程地址空间分配存储
kernel void CCTestFouncitionG(void)
{
//在线程空间分配空间给x,p
float x;
thread float p = &x;
}
常量地址空间修饰符constant
-
常量地址空间
指向的缓存对象
也是从设备内存池
分配存储,仅可读
- 在
程序域的变量
必须定义在常量地址空间
并且声明时初始化
,用来初始化的值必须是编译时的常量 - 在
程序域的变量
的生命周期和程序
一样,在程序中的并行计算着色函数
或者图形绘制着色函数
调用,但是constant的值
会保持不变
constant float samples[] = { 1.0f, 2.0f, 3.0f, 4.0f };
//对一个常量地址空间的变量进行修改也会失败,因为它只读的
sampler[4] = {3,3,3,3}; //编译失败;
//定义为常量地址空间声明时不赋初值也会编译失败
constant float a;
注意:常量地址空间
的指针或是引⽤
可以作为函数的参数
。 向声明为常量
的变量赋值
会产⽣编译错误。声明常量
但是没有赋予初值
也会产⽣编译错误
属性修饰符 -- 函数参数与变量的传递修饰符
图形绘制
或者并⾏计算着⾊器函数
的输⼊输出都是通过参数传递
。除了常量地址空间变量
和程序域
定义的采样器
以外
-
device buffer
:设备缓存, ⼀个指向设备地址空间的任意数据类型的指针或者引⽤; -
constant buffer
: 常量缓存区, ⼀个指向常量地址空间的任意数据类型的指针或引⽤ -
texture
:纹理对象; -
sampler
:采样器对象; -
threadGrounp
:在线程组中供各线程共享的缓存
属性修饰符的目的
- 参数表示资源的定位,可以理解为端口
- 在固定管线和可编程管线进行内建变量的传递
- 将数据沿着渲染管线从顶点函数传递片元函数.
传递修饰符在代码中的使用
- device buffer/constant buffer ---> [[buffer(index)]]
- texture ---> [[texture(index)]]
- sampler ---> [[sampler(index)]]
- threadGroup ---> [[threadGroup(index)]]
index
是⼀个unsigned integer
类型的值,它表示了⼀个缓存、纹理、采样器
参数的位置(在函数参数索引 表中的位置)。 从语法上讲,属性修饰符的声明位置应该位于参数变量名之后
在代码中如何表现:
1.已知条件:device buffer(设备缓存)/constant buffer(常量缓存)
代码表现:[[buffer(index)]]
解读:不变的buffer ,index 可以由开发者来指定.
2.已知条件:texture Object(纹理对象)
代码表现: [[texture(index)]]
解读:不变的texture ,index 可以由开发者来指定.
3.已知条件:sampler Object(采样器对象)
代码表示: [[sampler(index)]]
解读:不变的sampler ,index 可以由开发者来指定.
4.已知条件:threadgroup Object(线程组对象)
代码表示: [[threadgroup(index)]]
解读:不变的threadgroup ,index 可以由开发者来指定.
//并行计算着色器函数add_vectros ,实现2个设备地址空间中的缓存A与缓存B相加.然后将结果写入到缓存out.
//属性修饰符"(buffer(index))" 为着色函数参数设定了缓存的位置
//thread_position_in_grid:用于表示当前节点在多线程网格中的位置,并不需要开发者传递,是Metal自带的。
/*
kernel:并行计算函数修饰符
void:函数返回值类型
add_vectros:函数名
const device float4 *inA [[buffer(0)]]:定义了一个float4类型的指针,指向一个4维向量空间,放在设备内存空间(即显存GPU中)
- const device:只决定放在哪里
- inA:变量名
- [[buffer(0)]] 对应 buffer中0这个id
*/
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];
}
//着色函数的多个参数使用不同类型的属性修饰符的情况
//纹理读取的方式的sampler,即采样器,[[sampler(0)]]表示采样器的缓存id
kernel void my_kernel(device float4 *p [[buffer(0)]],
texture2d img [[texture(0)]],
sampler sam [[sampler(0)]])
{
//.....
}
内建变量修饰符
-
[[vertex_id]]
:顶点id
标识符,不由开发者传递 -
[[position]]
:在顶点函数
中表示当前的顶点信息--类型float4
,
描述片元的窗口的相对坐标(x,y,z,1/w)
,该像素点在屏幕上位置信息 -
[[point_size]]
:点的大小,类型是float
-
[[color(m)]]
:颜色,m
在编译前就必须确定 -
[[stage_in]]
:⽚元着⾊函数
使⽤的单个⽚元输⼊数据
是由顶点着⾊函数输出
然后经过光栅化
⽣成的。顶点和⽚元着⾊函数
都是只能有⼀个参数
被声明为使⽤stage_in
修饰符,对于⼀个使⽤了stage_in
修饰符的⾃定义的结构体,其成员可以为⼀个整形
或浮点标量
,或是整形
或浮点向量
。
//定义了片元输入的结构体,
struct MyFragmentOutput {
// color attachment 0 颜色附着点0
float4 clr_f [[color(0)]];
// color attachment 1 颜色附着点1
int4 clr_i [[color(1)]];
// color attachment 2 颜色附着点2
uint4 clr_ui [[color(2)]];
};
fragment MyFragmentOutput my_frag_shader( ... )
{
MyFragmentOutput f;
....
f.clr_f = ...;
....
return f;
}