Metal 一、初识 Metal 及其语言规范

Metal 简介 与 Metal 编程语言语法。

一、Metal 简介

1、Metal 是 Apple 为了解决 3D 渲染而推出的框架。游戏开发者的新的技术平台,该技术能够为3D图像提高 10 倍的渲染性能。苹果2018年推出 Metal,在此之前一直使用OpenGL ES。

Metal 的2个目的:1图形渲染; 2高并发计算

苹果文档给出的 Metal 优化:CPU 低消耗、GPU 高利用、连续处理器并行、有效的资源利用

Metal 官方文档

2、图形管道 graphic pipeline

二、Metal Shading Language - Metal 编程语言与规范

1、Metal 语言特点

1.1)Metal 着色器语言,用来编写3D 图形渲染逻辑和并行计算核心逻辑(高并发) 的一门编程语言。当我们需要使用 Metal 框架来完成 APP 的开发时(或 需要使用 Metal 的高并发计算能力时),就要使用 Metal 编程语言进行编程。

1.2)Metal 编程语言 使用 clang 和 LLVM 进行编译处理

Metal的语言规则,是基于 C++ 11.0 标椎设计的,并在此基础上进行了一定的扩展和限制。

而我们的实际业务开发场景中所需要做的工作就是: 编写 在GPU 上执行的图像渲染逻辑代码 以及 通用的并行计算逻辑 代码。

1.3)Metal 与 C++ 11.0 

1.3.1)Metal 限制(不支持):

  Lambda 表达式

  递归函数调用

  动态转换操作符

  类型识别

  对象创建 new 和 销毁 delete 操作符

  操作符 noexcept

  goto 跳转

  变量存储修饰符 register 和 thread_local

  虚函数修饰符

  派生类

  异常处理

  C++ 标准函数库 在 Metal 中不支持,不能使用

1.3.2)Metal 中指针的使用限制

  Metal 图形和并行计算函数用到的参数,如果是指针 必须使用地址空间修饰符(devide / threadgroup / constant) 

  不支持函数指针

  函数名不能出现 main

1.3.3)Metal 像素坐标系

  Metal 中纹理/帧缓冲区 attachment 的像素使用的坐标系的原点是在 左上角。--> 苹果的坐标原点

2、Metal 的数据类型

2.1)标量类型

 

2.2)向量与矩阵 数据类型

2.2.1)向量

booln

charn

shortn

intn

ucharn / ushortn / uintn

halfn

floatn

n --> 向量中的 n 表示维度,最大为4

代码示例:

bool2 b = [1, 2]; 

float4 f4 = float4(1.0, 2.0, 3.0, 4.0);
float f = f4[0];// x = 1.0 --> 类似数组

// int4 --> 4个变量组成的4维向量
// xyzw  rgba
int4 test = int4(0, 1, 2, 3);
int x = test.x;// x = 0
int y = text.y;// y = 1
int x1 = test.r;// x1 = 0

// 多个分量的访问
float4 c = float4(0,0,0,0);
c.xyzw = float4(1,2,3,4);// 重赋值 c = [1,2,3,4]
c.xy = float2(6,0);// c = [6,0,3,4]
c.yzw = float3(7,8,9);// c = [6,7,8,9]

// 可乱序 --> 
// 注意!!!这里 Metal 不同于 GLSL --> GLSL 可以多分量,但是不可乱序 xyzw/rgba 顺序是不可变的
float4 pos = c.wxyz;// pos = [9,6,7,8]

float4 rep = c.xxwz;// rep = [6,6,9,8]
rep.xw = float2(5,7);// rep = [5,6,9,7]

// 不可混用
float4 m = float4(4,3,2,1);
m.xg = float2(0,9);!error 非法, xyzw 和 rgba 不能混了,他俩只可选其一
m.rg = float(0,9);// m = [0,9,2,1]


/// 构造方式
// 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);

// 多个向量构造的使用
float x = 1.0f,y = 2.0f,z = 3.0f,w = 4.0f;
float4 a = float4(0.0f);
float4 b = float4(x,y,z,w);
float2 c = float2(5.0f,6.0f);
float2 a = float2(x,y);
float2 b = float2(z,w);
float4 x = float4(a.xy,b.xy);

2.2.2)矩阵

halfxnm / floatnxm

nxm 中 n m 分别指 矩阵的行数 和 列数 --> 最大 4 x 4 : 4行4列

float4x4 mix;
// mix[1] = float4(2.0f);// 矩阵 第一行的值都是 2
mix[1] = float4(1,2,3,4);// 矩阵的第一行的值
mix[0][0] = 3;// 矩阵的第0行0列的值为 3
mix[3][2] = 7;// 矩阵 3行2列 位置的值

2.3)纹理 Texture 类型

纹理类型是一个句柄,指向一个 一维/二维/三维纹理数据。在函数中描述纹理对象的类型。 

枚举: 

enum class access { sample, read, write } // 定义访问权限

sample:纹理对象可以被采样,采样器可将纹理读取出来,可读可写可采样 --> 使用最多

read:不使用采样器,一个图形渲染函数或并行计算函数 可以读取纹理对象

write:一个图形渲染函数或并行计算函数 可以向纹理对象写入数据。

texture1d<T, access a = access::sample> // 一维纹理
texture2d<T, access a = access::sample>// 二维纹理
texture3d<T, access a = access::sample>// 三维纹理

T:数据类型 ,指定从纹理中 读取/写入 时的颜色类型。T可以是 half、float、int 等;

access:读写方式(权限)

代码示例:

void foo (texture2d<float> imgA [[ texture(0) ]] ,// texture2d<float>: 2 维纹理,类型 float,访问权限 sample --> 默认权限就是 sample 可不写
texture2d<float, access::read> imgB [[ texture(1) ]],// texture2d<float, access::read>: 类型 float,权限 read
texture2d<float, access::write> imgC [[ texture(2) ]]) // 权限 write
{ 
 ... 
}

2.4)采样器类型 Samplers

采样器类型 决定了如何对一个纹理进行采样操作。

metal 框架中有一个对应 着色器语言的采样器对象:MTLSamplerState, 此对象做为 图像渲染着色器函数 or 并行计算函数 的参数进行传递。

枚举 们:

// 从纹理中采样时,纹理坐标是否归一化
enum class coord { normalized, peixel }

// 纹理采样过滤方式 - 放大/缩小
enum class filter { nearest, linear }

// 缩小过滤方式
enum class min_filter { nearest, linear }

// 放大过滤方式
enum class mag_filter { nearest, linear }

// 设置纹理 s t r 坐标的寻址模式 (str 即 xyz 环绕方式)
enum class s_address { clapm_to_zero, clapm_to_edge, repeat, mirrored_repeat }
enum class t_address { clapm_to_zero, clapm_to_edge, repeat, mirrored_repeat }
enum class r_address { clapm_to_zero, clapm_to_edge, repeat, mirrored_repeat }

// 设置所有纹理坐标的寻址模式
enum class address { clapm_to_zero, clapm_to_edge, repeat, mirrored_repeat }

// 设置纹理采样的 mipMap 过滤模式,如果是 none ,则只有一层纹理生效
enum class mag_filter { none, nearest, linear }

注意:在 Metal 中,初始化采样器必须使用 constexpr 修饰符声明

代码示例:

// 初始化 创建 采样器 属性设置
constexpr sampler s(coord::pixel, address::clamp_to_zero, filter::linear); // 

constexpr sampler a(coord::normalized);

constexpr sampler b(address::repeat); 

constexpr sampler s(address::clamp_to_zero, filter::linear);

3、修饰符

3.1)函数修饰符

kernel:表示该函数是一个 数据并行计算着色函数。我们要高效并发运算就用它。它可以被分配在 一维/二维/三维 线程组中去执行;--> 使用他修饰的函数 返回类型必须是 void

vertex:顶点着色函数。为顶点数据流中的 每个顶点数据 执行一次,然后为每个顶点生成数据输出到绘制管线;

fragment:片元着色函数,为 片元数据流中的 每个片元 与其关联 执行一次,然后将 每个片元生成的颜色数据 输出到绘制管线中;

注意1:只有 图形着色函数才能用 vertex/fragment 修饰。函数返回类型 可以用来辨认出它是为顶点 or 为每个像素 做计算的。返回 void 也可以但是无意义,因为顶点/片元函数本就是为了计算出相应数据 将数据传到绘制管线的。

注意2:被函数修饰符修饰的函数不能再调用 '被修饰符修饰的函数',否则编译失败。即:被函数修饰符修饰的函数们不能相互调用

例:kernel void func1 (...) {}; vertex float4 funcV1 (...) { func1(...) } --> 错误调用,无法编译

注意2:特定函数修饰,普通函数随意。

代码示例:

kernel void foo(...) {
        ...
}

3.2)变量 或 参数 的地址空间修饰符 Address Space

地址空间修饰符:用来指定 一个函数 参数/变量 被分配在内存中的哪块区域。

device:设备地址空间

threadgroup:线程组地址空间

constant:常量地址空间

thread: thread 地址空间

a、对于 图形着色器函数,是 指针 或 引用 类型的参数 必须定义为 device 或 const 地址空间

b、对于并行计算着色函数,对于是 指针 或 引用 的参数,必须使用 device 或 threadgroup 或 constant 修饰。

3.2.1)Device Address Space(设备地址空间)

device:设备地址空间 指向设备内存(显存)池分配出来的缓存对象,它可以是可读也可以是可写的;一个缓存对象可以被声明成一个 标量、向量、自定义结构体的指针或引用。

代码示例:

// an array of a float vector with 4 components 
device float4 *color;
// 定义个结构体
struct Foo { float a[3]; int b[2]; } // an array of Foo elements device Foo *my_info;

注意1:纹理对象 总是在设备地址空间分配内存,device 地址空间修饰符不必出现在纹理类型定义中。一个纹理对象的内容无法直接访问,Metal 提供了读写纹理的内建函数。

3.2.2)线程组地址空间 threadgroup

threadgroup:用于 为 并行计算着色函数 分配内存变量(在GPU里),这些变量被一个线程组的所有线程共享。在线程组地址空间 分配的变量 不能被用于图形绘制着色函数。

  在并行计算着色函数中,在线程组地址空间分配的变量 为一个线程组使用,生命周期和线程相同。

代码示例:

// kernel 高速并行
kernel void my_func(threadgroup float *a [[ threadgroup(0) ]],
...) 
{
    // A float allocated in threadgroup address space 
    threadgroup float x;
    // An array of 10 floats allocated in threadgroup address space
    threadgroup float b[10];
}

3.2.3)constant 常量地址空间

constant:指向的缓存对象也是从设备内存池 分配存储,但是是只读的。

在程序域的变量 必须定义在常量地址空间 并且在声明的时候初始化;用来初始化的值 必须是编译时的常量。此变量的生命周期和程序一样,在程序中的 并行计算着色函数or图形绘制着色函数 调用,但 constant 的值会保持不变。

注意:常量地址空间 的指针或引用 可以作为函数的参数(constant修饰的常量可作为函数的参数)。向声明为常量的变量赋值会产生变异错误(代码示例中sampler),声明为常量但没有赋予初始值也会产生变异错误(代码示例中a)。

错误代码示例:

constant float sampler[] = {1.0f, 2.0f, 3.0f,4.0f};

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

// 定义常量地址空间但不初始化赋值 --> 也编译失败
const float a;// 编译失败

3.2.4)线程地址空间 thread

thread:指向每个线程准备的地址空间,这个线程的地址空间 定义的变量 在其他线程是不可见的,在图形绘制着色函数or并行计算着色函数 中声明的变量可以使用 thread 地址空间分配。

代码示例:

kernel void func2 (...) {
    float x;
    thread float p = &x;
    ...
}

3.3)函数参数与变量

图形绘制/并行计算着色函数的 输入/输出 都需要通过参数传递 ( 除了常量地址空间变量和程序域中定义的采样器 外)。参数如下:

device buffer:设备缓存 - 指向设备地址空间的任意数据类型的指针 or 引用

constant buffer:常量缓存 指向常量地址空间的任意数据类型的指针 or 引用

texture object:纹理对象

sample object:采样器对象

threadgroup:线程共享的缓存

对于每个着色器函数来说,一个修饰符是必须指定的,它用来设定一个 缓存、纹理、采样器的位置:

device buffer / constant buffer --> [[buffer(index)]]

texture --> [[texture(index)]]

sample --> [[sampler(index)]]

threadgroup buffer --> [[threadgroup(index)]]

index:一个 unsigned integer 类型的值,表示一个 缓存、纹理、采样器的位置(在函数参数索引表中的位置)。语法上讲,属性修饰符的声明位置 应该位于参数变量名之后。

通过示例理解:

// 一个简单的并行计算着色函数 my_add ,它把两个设备地址空间的魂村 inA、inB 相加,把结果写入缓存 out。
// 属性修饰符 “buffer(index)” 为着色函数参数 设定了缓存的位置
kernel void my_add (constant device float4 *inA [[ buffer(0) ]],// inA: 放在设备地址空间,缓存位置对应的是 buffer(0)这个ID , constant 修饰的不可变
                                constant device float4 *inB [[ buffer(1) ]],
                                device float4 *out [[ buffer(2) ]],
                                uit id [[ thread_position_in_grid ]],) {

    out[id] = inA[id] + inB[id];
}            

thread_position_in_grid:用于表示当前节点,在多线程网格中的位置 --> 我们是无法知道当前在GPU的哪个运算单元里,thread_position_in_grid 知道,我们通过它获取即可。

3.4)内建变量属性修饰符

[[vertex_id]] -- 顶点ID标识符

[[position]] -- 1、当前顶点信息(float4 - xyzw)  2、也可描述 片元在窗口的相对坐标:当前这个像素点在屏幕上的哪个位置

[[point_size]] -- 点的大小

[[color(m)]] -- 颜色,m 编译前要确定

如下代码:

// 定义颜色结构体
struct myFragmentOutput {

    // 三组颜色,要知道使用时取哪一个
    float4 clr_f [[color(0)]];// 
    int4 clr_i [[color(1)]];//
    uint4 clr_ui [[color(2)]];//
}

fragment myFragmentOutput my_grag_shader (...) {

    myFragmentOutput f;
    ...
    f.clr_f = ...;
    ...
    return f;
}

另补充一个:[[stage_in]] -- 其实就是:顶点着色器输出 经过光栅化 生成的 传给片元着色器的 每个片元数据。

顶点和片元着色函数都是 有且仅有 一个参数可以被声明为 使用"stage_in"修饰符的。 stage_in 可以修饰结构体,其结构体成员可以有多个,类型可以为一个 整型/浮点型 的 标量/向量。

 

posted @ 2020-08-26 00:34  张张_z  阅读(3719)  评论(0编辑  收藏  举报