1-常规语言特性

开始学学Slang,照抄官方文档(

Slang是一种着色器语言,它通过模块化可拓展的方式方便地构建和维护大型Shader代码库,并且保证性能。Slang代码可以编译为D3D12, Vulkan, Metal, D3D11, OpenGL, CUDA,甚至是CPU上跑的代码,可谓“写一次shader,在任何平台上运行”。

Slang的语法和HLSL类似,接下来看看它的常规语言特性。

类型

Slang的常用类型包括:标量,向量,矩阵,数组,结构体,枚举和资源。

标量类型

整型

Slang提供如下整型:

类型描述
int8_t8位有符号整数
int16_t16位有符号整数
int32位有符号整数
int64_t64位有符号整数
uint8_t8位无符号整数
uint16_t16位无符号整数
uint32位无符号整数
uint64_t64位无符号整数

其中,所有平台支持32位的整数,64位只有部分平台支持。详见这里

浮点型

Slang提供如下浮点型:

类型描述
half16位浮点数
float32位浮点数
double64位浮点数

其中,所有平台支持32位的浮点数,64位只有部分平台支持。

布尔型

bool类型用于表达布尔类型的值:truefalse。该类型的大小依平台而定:

目标平台sizeof(bool)
GLSL4 字节 / 32位宽
HLSL4 字节 / 32位宽
CUDA1 字节 / 8位宽

当布尔型存储在结构体中时,需要注意内存对齐。

void

空返回值类型。

向量

向量类型可写作vector<T, N>,其中T是上面提到的标量类型,N是维数[2, 4]。此外为了方便写代码,还有一些预定义好的类型,格式是{T}{N}例如float3就是vector<float, 3>

矩阵

矩阵类型可写作matrix<T, R, C>,其中T是标量类型,RC都是[2, 4]的整数,分别是行和列。矩阵同样也有预定义好的类型{T}{R}x{C},例如float3x4就是matrix<float, 3, 4>

Slang的float3x4是3行4列的矩阵,而GLSL的mat3x4是4行3列的矩阵,需要注意区分。

数组

数组的类型则是T[N],例如:

// a的类型是int[3]
int a[3];
// 通过初始化列表确认数组的大小
int a[] = {1, 2, 3};

如果要作为函数中的参数,数组大小可以不写:

int f(int b[])
{
    // 该函数要求b的大小是可确认的, 否则会报错
    return b.getCount();	
}

void test()
{
    int arr[3] = {1, 2, 3};
    int x = f(arr);			// 3
}

在Slang中,数组是值类型,意思就是在赋值,传参的时候是值传递(复制)。

结构体

和C系列语言类似,结构体定义如下:

struct MyData
{
    int a;
    float b;
}

结构体类型可以拥有构造函数,关键字是__init

struct MyData
{
    int a;
    __init() { a = 5; }
    __init(int t) { a = t; }
}
void test()
{
    // 默认构造函数 d.a = 5
    MyData d;  
    // 重载构造函数 h.a = 4
    MyData h = MyData(4); 
}

需要注意的是,目前Slang还不允许让结构体成员有默认值,将来可能会实现这一特性。

枚举

enum声明枚举类型,它提供类型安全的常量(强类型枚举):

enum Channel
{
    Red = 5,
    Green,	// 6
    Blue	// 7
}

如果不指定元素的默认值,默认是从0开始自增。

可以显式指定枚举元素的类型为某个整型类型,默认是int

enum Channel : uint16_t
{
    Red, Green, Blue
}

枚举类型支持内置的ILogical接口,因此它可以进行位运算:

void test()
{
    Channel c = Channel.Red | Channel.Green;
}

为了方便位运算,可以在声明的上一行用[Flag]标识,让元素以2的幂自增:

[Flags]
enum Channel
{
    Red,   //  = 1 = 2^0
    Green, //  = 2 = 2^1
    Blue,  //  = 4 = 2^2
    Alpha, //  = 8 = 2^3
}

枚举默认是强类型的。如果要使用弱类型枚举,需要在声明上一行用[UnscopedEnum]

[UnscopedEnum]
enum Channel
{
    Red, Green, Blue
}
void test(Channel c)
{
    if (c == Red) { /*...*/ }
}

不透明类型

Slang核心模块定义了许多不透明类型(Opaque Type),用于访问通过GPU API分配的对象。不透明类型(及还有该类型的结构体或数组)可能会被如下原因限制(因平台而异):

  • 返回不透明类型的函数可能不被允许;
  • 不透明类型的全局和静态变量可能不被允许;
  • 不透明类型出现在buffer的元素类型中,除非明确表示允许外,该行为可能不被允许;

纹理类型

纹理类型(如Texture2DTextureCubeArrayRWTexture2D)用于读写、采样纹理数据。完整的纹理类型定义如下:

{访问权限}Texture{基础形状}{是否多重采样}{是否数组}{元素类型}

其中:

  • 访问权限:只读(不写),读写(RW),以所给资源的光栅化顺序读写(RasterizerOrdered);
  • 基础形状:1D2D3DCube
  • 是否多重采样:是的话就要加上MS
  • 是否是数组:是的话就要加上Array
  • 元素类型:可显示指定,默认为float4

需要注意的是,不是所有平台都支持如上修饰符的任意组合。

缓冲区

现代图形API支持多种缓冲区:

Formatted Buffer

类似1D纹理,支持加载时的格式转换,但不支持Mipmap。格式缓冲区的定义如下:

{访问权限}Buffer{是否是数组}{元素类型}

例如Buffer<float4>是存储能被获取为float4的一个GPU资源,它在GPU的内部存储类型不确定,可能是RGBA8

Flat Buffer

和Formatted Buffer不同,它不支持格式转换。Flat Buffer主要有两个类型:

  • Structured Buffer:例如StructuredBuffer<T>,包含能读取和存储的T类型Buffer;
  • Byte addressed buffer:例如ByteAddressBuffer,不特定于任何寻常的元素类型,它允许从缓冲区中任何字节(注意对齐)偏移量中加载或存储值。

这两个缓冲区都能用{访问权限}控制读写使用权限。

Constant Buffer

也被称为Uniform Buffer,用于从CPU向GPU传递数据。和上面两种缓冲区不同,ConstantBuffer<T>只允许单个T类型的值,而不是一个或多个。

表达式

Slang的表达式和HLSL,GLSL,C/C++类似:

  • 字面量:123
  • 成员访问:StructValue.fieldMyEnumType.CaseOne
  • 调用函数:sin(a)
  • 向量/矩阵初始化:int4(1, 2, 3, 4)
  • 类型转换:(int)xdouble(0, 0)
  • 索引:a[i]
  • 初始化列表:int b[] = {1, 2, 3};
  • 赋值:l = r
  • 运算符:-a, b + c, d++, e %= f

Slang也有&&||逻辑运算符,但目前不支持短路操作。

向量和矩阵的运算符

普通的一元运算符和二元运算符也可以应用于向量和矩阵。需要注意的是乘法的书写习惯:例如GLSL和Slang中矩阵定义不同,乘法顺序就不同。GLSL中是mat3x4 * vec3,Slang中则是mul(float3, float3x4)

Swizzle

swizzle操作可以解构一个或多个向量。例如vfloat4,那么v.xy就是float2

和GLSL不同,Slang只支持xyzwrgba形式的解构。

和HLSL不同,Slang目前不支持矩阵的Swizzle操作。

语句

Slang支持如下语句:

  • 表达式语句:f(a, 3);, a = b * c;
  • 局部变量声明: int x = 99;
  • 代码块: { ... }
  • 空语句: ;
  • if 语句
  • switch 语句
  • for 语句
  • while 语句
  • do-while 语句
  • break 语句
  • continue 语句
  • return 语句

Discard语句

discard语句可用于片段着色器中,它可以结束当前片段着色器的执行,让图形系统丢弃此片段。

函数

有两种定义方式,C风格的和现代的:

float addSomeThings(int x, float y)
{
    return x + y;
}

func addSomeThings(x : int, y : float) -> float
{
    return x + y;
}

此外,还能标识函数参数的输入输出:

  • in:默认,值传递的输入参数;
  • out:标识该参数用于输出,类似于C++的非常量引用传递;
  • inoutin out:标识该参数是输入/输出参数。

预处理器

Slang支持如下C风格的预处理器:

  • #include
  • #define
  • #undef
  • #if, #ifdef, #ifndef
  • #else, #elif
  • #endif
  • #error
  • #warning
  • #line
  • #pragma, 包括 #pragma once

需要注意的是,不推荐用#include,因为Slang还提供了模块系统,详见之后的文章。

属性

属性(Attributes)用于装饰声明和语句,让它们拥有元数据。例如之前提到的[Flag]等。

全局变量和着色器参数

Slang中的全局变量都是从CPU程序传向GPU的参数。如果不想让全局变量被视为着色器参数,必须用static标识它:

// a shader parameter:
float a;

// also a shader parameter (despite `const`):
const int b = 2;

// a "thread-local" global variable
static int c = 3;

// a compile-time constant
static const int d = 4;

全局常量

全局static const变量定义了一个编译期常量。

全局静态变量

只受static修饰,类似于C++的全局变量,但是每线程独立存储,不是真正的“全局”。它的生命周期只存在于它的线程。

需要注意的是,部分平台不支持全局静态变量。

全局着色器参数

适用于任何类型,包括不透明和透明类型:

ConstantBuffer<MyData> c;
Texture2D t;
float4 color;

这样做Slang会给警告,因为它不知道用户将该变量作为着色器参数还是“全局变量”。消除警告的方法就是给它标识uniform

// WARNING: this declares a global shader parameter, not a global variable
int gCounter = 0;

// OK:
uniform float scaleFactor;

旧版 Constant Buffer

为了和存在的HLSL代码兼容,Slang也支持全局的cbuffer

cbuffer PerFrameCB
{
    float4x4 mvp;
    float4 skyColor;
    // ...
}

当然也能用上面提到的ConstantBuffer<T>来写:

struct PerFrameData
{
    float4x4 mvp;
    float4 skyColor;
    // ...
}
ConstantBuffer<PerFrameData> PerFrameCB;

显式绑定标记

为了和存在的代码库兼容,Slang支持特定API的绑定标记。

例如Direct3D可能用register绑定信息:

Texture2D a : register(t0);
Texture2D b : register(t1, space0);

Vulkan(和OpenGL)通过[[vk::binding(...)]]属性绑定信息:

[[vk::binding(0)]]
Texture2D a;

[[vk::binding(1, 0)]]
Texture2D b;

一个Slang参数可被以上两种风格的绑定标记同时修饰,但需要注意的是显式绑定标记只适用于它专用的API。

实际上显式绑定标记没必要写,且容易出错。Slang的编译器会自动帮你安排好。

着色器程序入口

GPU着色器程序的执行入口本质上就是一个函数:

[shader("vertex")]
float4 vertexMain(
    float3 modelPosition : POSITION,
    uint vertexID : SV_VertexID,
    uniform float4x4 mvp)
    : SV_Position
{ /* ... */ }

程序入口属性和阶段

Slang中,属性[shader(...)]用于标记一个着色器程序的入口,并且也标识了当前的渲染阶段。光栅化,计算着色器,和光追管线都有它们各自的渲染阶段,并且新版本图形API可能会引入新的阶段。

入口参数

入口函数的参数有 可变Varying均匀Uniform 两种:

  • 可变入参:同一批次(如同一draw call,计算分配)中的线程,入参的值可能不同;
  • 均匀入参:保证同一批次中的线程,入参的值相同;

Slang中的入口参数默认是可变的,要想将其变为均匀的,得添加uniform关键字。

绑定语义

可变的入口参数必须声明一个绑定语义(Binding Semantic),以查明这些参数如何与执行环境联系。声明绑定语义的格式如下:

{参数} : {语义}

例如上边的uint vertexID : SV_VertexID

绑定语义有两种:

  • 系统定义的绑定语义:标准系统定义的绑定语义以SV_开头。

    对于输入参数,它们应该从GPU接受特定数据,并为程序所用。例如顶点着色器中的SV_VertexID,通过它可获取当前线程中正在处理的顶点ID。

    对于输出参数或shader程序的返回值,GPU 应以所使用的管线和阶段定义的特定方式使用存储在该输出中的值。例如顶点着色器中的SV_Position,它表示应该和光栅化器交流的裁剪空间位置。

    允许输入/输出参数使用的系统定义的绑定语义受当前管线和阶段影响。

  • 用户定义的绑定语义:例如上边modelPosition的绑定语义POSITION就是用户自定义的。

    对于输入参数,应该从上一阶段中找到匹配的绑定语义,接收数据。

    对于输出参数,应该向下一阶段中找到匹配的绑定语义,提供数据。

多个着色器程序入口

Slang支持单文件多个程序入口,可以将有关联的多个程序入口写在一个文件中。例如以下光追的例子:

struct Payload { float3 color; };

[shader("raygeneration")]
void rayGenerationProgram() {
    Payload payload;
    TraceRay(/*...*/, payload);
    /* ... */ 
}

[shader("closesthit")]
void closestHitProgram(out Payload payload) { 
    payload.color = {1.0};
}

[shader("miss")]
void missProgram(out Payload payload) { 
    payload.color = {1.0};
}

已道心破碎,以后就挑自己觉得重要的抄吧。感觉还不一定能用到我的OpenGL项目上,唉。

参考资料

  • shader-slang/slang: Making it easier to work with shaders (github.com)
  • Conventional Language Features | slang (shader-slang.org)