1-常规语言特性
开始学学Slang,照抄官方文档(
Slang是一种着色器语言,它通过模块化可拓展的方式方便地构建和维护大型Shader代码库,并且保证性能。Slang代码可以编译为D3D12, Vulkan, Metal, D3D11, OpenGL, CUDA,甚至是CPU上跑的代码,可谓“写一次shader,在任何平台上运行”。
Slang的语法和HLSL类似,接下来看看它的常规语言特性。
类型
Slang的常用类型包括:标量,向量,矩阵,数组,结构体,枚举和资源。
标量类型
整型
Slang提供如下整型:
类型 | 描述 |
---|---|
int8_t | 8位有符号整数 |
int16_t | 16位有符号整数 |
int | 32位有符号整数 |
int64_t | 64位有符号整数 |
uint8_t | 8位无符号整数 |
uint16_t | 16位无符号整数 |
uint | 32位无符号整数 |
uint64_t | 64位无符号整数 |
其中,所有平台支持32位的整数,64位只有部分平台支持。详见这里。
浮点型
Slang提供如下浮点型:
类型 | 描述 |
---|---|
half | 16位浮点数 |
float | 32位浮点数 |
double | 64位浮点数 |
其中,所有平台支持32位的浮点数,64位只有部分平台支持。
布尔型
bool
类型用于表达布尔类型的值:true
和false
。该类型的大小依平台而定:
目标平台 | sizeof(bool) |
---|---|
GLSL | 4 字节 / 32位宽 |
HLSL | 4 字节 / 32位宽 |
CUDA | 1 字节 / 8位宽 |
当布尔型存储在结构体中时,需要注意内存对齐。
void
空返回值类型。
向量
向量类型可写作vector<T, N>
,其中T
是上面提到的标量类型,N
是维数[2, 4]
。此外为了方便写代码,还有一些预定义好的类型,格式是{T}{N}
例如float3
就是vector<float, 3>
。
矩阵
矩阵类型可写作matrix<T, R, C>
,其中T
是标量类型,R
和C
都是[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的元素类型中,除非明确表示允许外,该行为可能不被允许;
纹理类型
纹理类型(如Texture2D
,TextureCubeArray
,RWTexture2D
)用于读写、采样纹理数据。完整的纹理类型定义如下:
{访问权限}Texture{基础形状}{是否多重采样}{是否数组}{元素类型}
其中:
- 访问权限:只读(不写),读写(
RW
),以所给资源的光栅化顺序读写(RasterizerOrdered
); - 基础形状:
1D
,2D
,3D
,Cube
; - 是否多重采样:是的话就要加上
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.field
,MyEnumType.CaseOne
- 调用函数:
sin(a)
- 向量/矩阵初始化:
int4(1, 2, 3, 4)
- 类型转换:
(int)x
,double(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操作可以解构一个或多个向量。例如v
是float4
,那么v.xy
就是float2
。
和GLSL不同,Slang只支持
xyzw
和rgba
形式的解构。和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++的非常量引用传递;inout
或in 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)