Metal之Shading Language Specification(着色语言规范)

Metal简述

  • Metal着色器语言是用来编写 3D图形渲染逻辑、并行Metal计算核心逻辑 的一门编程语言,当你使用Metal框架来完成APP的实现时则需要使用Metal编程语言。
  • Metal语言使用Clang 和LLVM进行编译处理,编译器对于在GPU上的代码执行效率有更好的控制。
  • Metal基于C++ 11.0语言设计的,在C++基础上多了一些扩展和限制,主要用来编写在GPU上执行的图像渲染逻辑代码以及通用并行计算逻辑代码。
  • Metal 像素坐标系统:Metal中纹理 或者 帧缓存区attachment的像素使用的坐标系统的原点是左上角。
  • Metal 与 C++ 11.0:Metal 这⻔语⾔是基于C++ 11.0标准设计的, 它在C++基础是⾏多了⼀些拓展和限制。

Metal Restrictions 限制

① Metal中不支持C++11.0的如下特性:

  • Lambda表达式
  • 递归函数调用
  • 动态转换操作符
  • 类型识别
  • 对象创建new和销毁delete操作符
  • 操作符noexcept
  • go跳转
  • 变量存储修饰符 register 和thread_local
  • 虚函数修饰符
  • 派生类
  • 异常处理

② C++标准库在Metal语言中也不可使用

③ Metal语言对于指针使用的限制

  • Metal图形和并行计算函数用到的入参(比如指针 / 引用),如果是指针 / 引用必须使用地址空间修饰符(比如device、threadgroup、constant)
  • 不支持函数指针
  • 函数名不能出现main

Metal数据类型

一、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 表示一个空的值集合
  • 常用的主要有 bool、int、uint 、half
  • undigned char 可以简写为 uchar
  • unsigned short 可以简写为 ushort
  • unsigned int 可以简写为 uint
  • 其中 half 相当于OC中的float,float 相当于OC中的double
  • size_t用来表示内存空间, 相当于OC中 sizeof

二、Metal支持的向量类型

① 支持类型
  • booln、charn、shortn、intn、ucharn、ushortn、uintn、halfn、floatn,其中 n 表示向量的维度,最多不超过4维向量;
  • 在OpenGL ES的GLSL语言中,例如2.0f,在着色器中书写时,是不能加f,写成2.0,而在Metal中则可以写成2.0f,其中f可以是大写,也可以是小写;
② 访问规则
  • 通过向量字母获取元素: 向量中的向量字母仅有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中向量不能乱序访问
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;

三、Metal支持的矩阵类型

① 支持类型
  • halfnxm、floatnxm,其中 nxm表示矩阵的行数和列数,最多4行4列,其中half、float相当于OC中的float、double;
  • 普通的矩阵其本质就是一个数组;
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构成,表示一行都是这个值
    4个float构成
    2个float2构成
    1个float2+2个float构成(顺序可以任意组合)
    1个float2+1个float
    1个float4
// float4类型向量的所有可能构造方式
// 1个一维向量,表示一行都是x
float4(float x);/
// 4个一维向量 --> 4维向量
float4(float x,float y,float z,float w);
// 2个二维向量 --> 4维向量
float4(float2 a,float2 b);
// 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个三维向量+1个一维向量 --> 4维向量
float4(float3 a,float b);
float4(float a,float3 b);
// 1个四维向量 --> 4维向量
float4(float4 x);

Metal其他类型

一、纹理类型
  • 纹理类型是一个句柄,指向一维/二维/三维纹理数据,而纹理数据对应一个纹理的某个level的mipmap的全部或者一部分;

  • 纹理的访问权限
    在一个函数中描述纹理对象的类型, access枚举值由Metal定义,定义了纹理的访问权利 enum class access {sample, read, write};,有以下3种访问权利,当没设定access时,默认的access 就是 sample;
    ① sample: 纹理对象可以被采样(即使用采样器去纹理中读取数据,相当于OpenGL ES的GLSL中sampler2D),采样一维这时使用 或者 不使用都可以从纹理中读取数据(即可读可写可采样);
    ② read:不使用采样器,一个图形渲染函数 或者 一个并行计算函数可以读取纹理对象(即仅可读);
    ③ write:一个图形渲染函数或者一个并行计算可以向纹理对象写入数据(即可读可写);

  • 定义纹理类型
    描述一个纹理对象/类型,有以下三种方式,分别对应一维/二维/三维,其中T代表 泛型,设定了从纹理中读取数据 或是 写入时的颜色类型,T可以是half、float、short、int等
    access表示纹理访问权限,当access没设定时,默认是sample:
    texture1d
    texture2d
    texture3d

// 类型 变量 修饰符
/*
 类型
    - 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<float> imgA[[texture(0)]],
          texture2d<float,access::read> imgB[[texture(1)]],
          texture2d<float,access::write> imgC[[texture(2)]]) {
    
    //...
}
二、采样器类型 Samplers

采样器类型决定了如何对一个纹理进行采样操作,在Metal框架中有一个对应着色器语言的采样器的对象MTLSamplerState,这个对象作为图形渲染着色器函数参数或是并行计算函数的参数传递,有以下几种状态:

  • 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 };
  • 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 };
  • mip_filter:设置纹理采样的mipMap过滤模式, 如果是none,那么只有一层纹理生效; enum class mip_filter { none, nearest, linear };
    Metal之Shading Language Specification(着色语言规范)_第1张图片
  • 注意:
    openGL ES中纹理坐标对应的是stq,Metal中纹理坐标对应是str;
    在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 着⾊器语⾔⽀持下列的函数修饰符:

  • kernel:表示该函数是一个数据并行计算着色函数,它可以被分配在一维/二维/三维线程组中去执行,表示函数要并行计算,其返回值类型必须是void类型,是一个高并发函数;
  • vertex:表示该函数是一个顶点着色函数,它将为顶点数据流中的每个顶点数据执行一次,然后为每个顶点生成数据输出到绘制管线;
  • fragment:表示该函数是一个片元着色函数,它将为片元数据流中的每个片元 和其相关联的数据执行一次,然后将每个片元生成的颜色数据输出到绘制管线中;

注意:

  • 使用kernel修饰的函数,其返回值类型必须是void类型
  • 一个被函数修饰符修饰的函数不能在调用其他也被函数修饰符修饰的函数,这样会导致编译失败,即Kernel、vertex、fragment修饰的函数不能相互调用,也不能同修饰符函数相互调用,但是可以调用普通函数;
  • 被函数修饰符修饰过的函数,只允许在客户端对其进行操作. 不允许被普通的函数调用;
  • Metal中并不是所有函数都需要上述3个修饰符修饰,是可以在Metal中定义普通函数的,即不带任何修饰符的函数;
  • 只有图形着色函数才可以被vertex和fragment修饰,对于图形着色函数,通过返回值类型可以辨认出是为顶点计算还是像素计算,其返回值也可以是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着色器语言使用地址空间修饰符来表示一个函数变量或者参数变量被分配于哪一片内存区域,有以下4中地址空间修饰符:

  • device: 设备地址空间
  • threadgroup: 线程组地址空间
  • constant 常量地址空间
  • thread 线程地址空间

② 注意:

  • 所有的着色函数(vertex、fragment、kernel)的参数,如果是指针/引用,都必须带有地址空间修饰符号;
  • 对于图形着色器函数(即vertex/fragment修饰的函数),其指针/引用类型的参数必须定义为 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:设备地址空间修饰符
  • 设备地址空间指向设备内存池分配出来的缓存对象(设备指显存,即GPU),即GPU空间分配的缓存对象,它是可读可写的,一个缓存对象可以被声明成一个标量、向量或是用户自定义结构体的指针/引用;
  • device放在变量类型之前;
  • 除了可以修饰 图形着色器函数 / 并行计算函数参数,还可以修饰指针变量 和 结构体指针变量:
// 设备地址空间: device 用来修饰指针.引用
// 修饰指针变量
device float4 *color;

struct CCStruct{
    float a[3];
    int b[2];
};
// 修饰结构体类的指针变量
device CCStruct *my_CS;
  • 纹理对象总是在设备地址空间分配内存,即纹理对象默认在GPU分配内存;
  • device地址空间修饰符不必出现在纹理类型定义中;
  • 一个纹理对象的内容无法直接访问,Metal提供读写纹理的内建函数,通过内建函数访问纹理对象;
三、threadgroup:线程组地址空间修饰符
  • 线程组地址空间用于为并行计算着色器函数分配内存变量,这些变量被一个线程组的所有线程共享,在线程组地址空间分配的变量不能用于图形绘制着色函数(即顶点着色函数 / 片元着色函数),即在图形绘制着色函数中不能使用线程组;
  • 在并行计算着色函数中,在线程组地址空间分配的变量为一个线程组使用,生命周期和线程组相同;
/*
 1. threadgroup 被并行计算计算分配内存变量, 这些变量被一个线程组的所有线程共享. 在线程组分配变量不能被用于图像绘制
 2. thread 指向每个线程准备的地址空间. 在其他线程是不可见切不可用
 */
kernel void CCTestFouncitionF(threadgroup float *a) {
    // 在线程组地址空间分配一个浮点类型变量x
    threadgroup float x;
    
    // 在线程组地址空间分配一个10个浮点类型数的数组y;
    threadgroup float y[10];
    
}
四、constant:常量地址空间修饰符
  • 常量地址空间指向的缓存对象也是从设备内存池分配存储,仅可读;
  • 在程序域的变量必须定义在常量地址空间并且声明时初始化,用来初始化的值必须是编译时的常量;
  • 在程序域的变量的生命周期和程序一样,在程序中的并行计算着色函数或者图形绘制着色函数调用,但是constant的值会保持不变;
  • 常量地址空间的指针/引用可以作为函数的参数,向声明为常量的变量赋值会产生编译错误;
  • 声明常量但是没有赋予初值也会产生编译错误;
constant float samples[] = { 1.0f, 2.0f, 3.0f, 4.0f };

// 对一个常量地址空间的变量进行修改也会失败,因为它只读
sampler[4] = {3,3,3,3}; //编译失败; 

// 定义为常量地址空间声明时不赋初值也会编译失败
constant float a;
五、thread:线程地址空间修饰符
  • 线程地址空间指向每个线程准备的地址空间,也是在GPU中,该线程的地址空间定义的变量在其他线程不可见(即变量不共享);
  • 在图形绘制着色函数 或者 并行计算着色函数中声明的变量,在线程地址空间分配存储;
kernel void CCTestFouncitionG(void) {
    // 在线程空间分配空间给x,p
    float x;
    thread float p = &x;
}

内建变量修饰符

  • [[vertex_id]] :顶点id标识符,并不由开发者传递;
  • [[position]]: 在顶点着色函数中,表示当前的顶点信息,类型是float4; 还可以表示描述了片元的窗口的相对坐标(x,y,z,1/w),即该像素点在屏幕上的位置信息;
  • [[point_size]] :点的大小,类型是float;
  • [[color(m)]] :颜色,m在编译前就必须确定;
  • [[stage_in]] :片元着色函数使用的单个片元输入数据是由顶点着色函数输出然后经过光栅化生成的(即由顶点着色函数之后的颜色传递到片元着色函数),类似于GLSL中的varying传递纹理/颜色;
    顶点和片元着色器函数都只能有一个参数被声明为使用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; 
}

你可能感兴趣的:(iOS高级进阶,Swift高级进阶,Metal,ShadingLanguage,Restrictions限制,数据类型,地址空间修饰符,内建变量修饰符)