当前位置:网站首页>SPIR-V初窥
SPIR-V初窥
2022-07-06 01:13:00 【zenny_chen】
SPIR-V的概述
SPIR-V着色器被嵌入在 模块 之中。每个模块可以包含一个或多个着色器。每个着色器具有一个入口点,该入口点具有一个名字和一个着色器类型,着色器类型用于定义当前着色器跑在哪个着色 阶段 。入口点则是当前着色器开始执行的位置。一个SPIR-V模块伴随着创建信息被传递给Vulkan,然后Vulkan返回表示该模块的一个对象。该模块对象随后可以用于构造一条 流水线。这就是一单个着色器完整编译的版本,伴随着在当前设备上要运行它所需要的信息。
SPIR-V的表示
SPIR-V对于Vulkan而言是仅有的官方支持的着色语言。它在API层被接受并且最终用于构造流水线,这些流水线是配置一个Vulkan设备的对象,为你的应用完成工作。
SPIR-V被设计为对一些工具和驱动而言非常容易处理的表示。这通过不同实现之间的多样性来提升可移植性。一个SPIR-V模块的内部表示是一条32位字的流,存放在存储器中。除非你是一位工具写手或计划自己生成SPIR-V,否则的话你不太需要直接处理SPIR-V的二进制编码。而是说,你要么可以看SPIR-V的可读的文本表示,或是使用诸如 glslangvalidator 这样的官方Khronos GLSL编译工具来生成SPIR-V。
下面我们可以写一个计算着色器源文件,命名为 simpleKernel.comp
,然后拿给 glslangvalidator 去编译。其中 .comp
后缀名能告诉 glslangvalidator 该着色器将作为一个计算着色器进行编译。
#version 460 core
void main(void)
{
// Do Nothing...
}
然后我们使用以下命令行(笔者用的是Windows环境):
%VK_SDK_PATH%/Bin/glslangValidator -o simpleKernel.spv -V100 simpleKernel.comp
随后我们就能看到生成了一个名为 simpleKernel.spv 的SPIR-V二进制文件。我们可以使用SPIR-V反汇编器对该二进制文件再进行反汇编。我们使用 spirv-dis 这一官方反汇编工具。
%VK_SDK_PATH%/Bin/spirv-dis -o simpleKernel.spvasm simpleKernel.spv
以上命令将会输出人可读的汇编文件 simpleKernel.spvasm。其内容如下:
; SPIR-V
; Version: 1.0
; Generator: Khronos Glslang Reference Front End; 10
; Bound: 6
; Schema: 0
OpCapability Shader
%1 = OpExtInstImport "GLSL.std.450"
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
OpSource GLSL 460
OpName %main "main"
%void = OpTypeVoid
%3 = OpTypeFunction %void
%main = OpFunction %void None %3
%5 = OpLabel
OpReturn
OpFunctionEnd
我们可以看到SPIR-V的文本形式看上去像一种古怪的汇编语言的变种。我们可以逐行过一下上述反汇编内容,然后看看它是如何与原始的GLSL输入相关联的。上面所输出汇编的每一行表示一单条SPIR-V指令,一条指令可能会由多个符号(token)构成。此外,分号(;
)开头的语句为一条注释。
流中的第一条指令是 OpCapability Shader
,它请求开启着色器能力。SPIR-V功能被粗略地划分为 指令 和 特征。在你的着色器能使用任一这些特征之前,必须声明它将使用哪种特征作为其一部分。上述代码中的着色器是一个图形着色器并从而使用 Shader 这一能力。随着我们介绍更多SPIR-V以及Vulkan功能,我们将会介绍每种特征所依赖的各种不同的能力。
接着,我们看到 %1 = OpExtInstImport "GLSL.std.450"
。这本质上是导入额外的一组相应于GLSL版本450所包含的功能,而这往往也是原始着色器所写入的。注意,这条指令最前面写有 %1 =
。这是对当前指令的结果赋上一个ID。OpExtInstImport
的结果在效果上是一个库。当我们想调用此库中的函数时,我们就使用 OpExtInst
这条指令来实现。它具有两个操作数,分别是一个库(OpExtInstImport
指令的结果)和一个指令索引。这允许SPIR-V指令集可被任意扩展。
接着,我们看到了某些额外的声明。OpMemoryModel
为此模块指定了工作存储器模型,在上述代码例子中是对应于GLSL版本450的逻辑存储器模型。这意味着所有存储器访问所有存储器访问通过资源执行而不是通过一个物理存储器模型。而物理存储器模型访问存储器时是直接通过指针。
接着是对当前模块的入口点的声明。OpEntryPoint GLCompute %main "main"
指令意味着有一个入口点对应于OpenGL计算着色器,所导出的函数名为 main,而这里所指定的ID为 %main
,该符号将会被 spirv-as 汇编器根据上下文来分配一个具体的ID号。从上述代码中我们看到已经分配了 %1
、%3
和 %5
,而唯独没有 %2
和 %4
。而这两个ID号很有可能最终在被编译的时候会被 %main
和 下面一个符号 %void
所分配。这里的函数名 "main"
用于引用入口点,当我们将所产生的着色器模块递回给Vulkan时需要此模块的入口点。
然后我们会在后一条指令中使用上面的 %main
这个符号—— OpExecutionMode %main LocalSize 1 1 1
定义了此着色器的执行组大小为 1×1×1 个工作项。如果局部大小 layout
修饰符缺省的话,那么GLSL会隐式指定local size为 1×1×1。
下面的两条指令只是简单地提供一些信息。OpSource GLSL 460
指明当前模块是用GLSL版本460进行编译的,而 OpName %main "main"
为具有ID为 %main
的符号提供了一个名字。
现在我们就可以来看看这个 main 函数真正有料的部分。首先,%void = OpTypeVoid
声明了我们想用 %void
这一符号作为类型 void
。正如前面所述,它最终可能会被分配的ID为2。在SPIR-V中所有一切都具有一个ID,甚至是类型定义。较大的聚合类型可以通过顺序地引用更小的、更简单的类型进行构造。然而,我们需要从某个地方开始,而这里将一个类型分配给 void
正是我们开始的地方。
%3 = OpTypeFunction %void
意味着我们定义一个为3的ID作为一个函数类型,该函数类型取了 void
作为其返回类型(先前声明为 void
)并且没有任何参数。我们在下面一行使用了该类型。%main = OpFunction %void None %3
这意味着我们声明了一个符号 %main
(最终被分配的ID可能为4,而我们之前用它命名了 "main"
)作为函数3(上面那条语句)的一个实例,它返回类型为 void
,并且没有其他特别的声明。这是通过指令中的 None
进行指示,而该位置上其他可用的参数包括是否内联、或是该变量是否为常量等等。
最后,我们看到对一个标签(标签没有被使用,而只是编译器操作所留下的副作用)、隐式的返回语句、以及最终函数结束的声明。这就是我们SPIR-V的末尾。
设计原则
- 规律性:所有指令均以指示当前指令有多长的一个字的个数开头。这允许SPIR-V模块不需要去解码每个操作码。所有指令都具有一个操作码来支配所有的操作数,确定它们属于哪种操作数。对于具有可变个数操作数的指令,可变操作数的个数通过将该指令的字的总个数减去非可变的字的个数得到。
- 非组合性:没有组合类型暴露,也不需要对类型的大型编码/译码表。取而代之的是,类型是参数化的。图像类型声明了它们的维数、阵列等等。一切都是正交的,这极大地简化了代码。这对于其他类型也是类似的。这也应用于操作码。操作对于标量/向量大小都是正交的,但对于整数与浮点数之间的区别并不是正交的。
- 无模型:当指定了一个给定的执行模型(比如流水线阶段)之后,内部操作本质上是无模型的:一般来说,它遵循这个规则:“同一个拼写具有相同的语义”,并从而不会具有修改语义的模式比特位。如果对SPIR-V的改变修改了语义,那么它应该是一种不同的拼写。这使得SPIR-V的消费者更为健壮。确实存在声明的执行模式,但这些通常影响了模块与其执行环境交互的方式,而不是其内部语义。也有能力被声明,但这是要声明要被使用的功能子集,而不是去改变要被使用的任何语义。
- 声明式的:SPIR-V声明了外部可见的模式,像“写深度”,而不是具有要求从完整着色器观察进行推导的规则。它也显式地声明了将要使用什么寻址模式、执行模型、扩展指令集等等。
- SSA:中间操作的所有结果都是严格的SSA。然而,在存储器中驻留的所声明的变量、以及用于访问的加载/存储,并且这样的变量可以被存储多次。
- IO:某些存储类是用于输入/输出(IO)的,并且从根本上,IO是通过在这些存储类中所声明的变量的加载/存储来完成的。
静态单一赋值(SSA)
SPIR-V包含了一条phi指令以允许将中间结果从分裂的控制流合并在一起。这允许控制流不需要加载/存储到存储器。SPIR-V在加载/存储的使用程度上是很灵活的;不用phi指令来使用控制流也是可能的,而通过用存储器的加载/存储仍然遵循着SSA形式。
某些存储类是用于IO的,而且在根本上IO是通过存储/加载实现的,而初始的加载和最终的存储不会被消除。其他存储类是着色器本地的,这可以将它们的加载/存储进行消除。我们可以认为对这种加载/存储通过将它们搬移到SSA形式的中间结果进行巨大的消除是一种优化。
内建变量
SPIR-V以一个枚举值装饰从一个高级语言来标识内建变量。这将任一不寻常的语义分配给该变量。内建变量除此以外以它们正确的SPIR-V类型进行声明,并且跟其他变量一样对待。
特化
特化允许基于常量值的一个可移植的SPIR-V模块进行离线创建,而此常量一开始是未知的,直到后面某个时间点。比如,在创建一个模块期间,具有一个常量固定大小数组,其常量值是未知的,从而该数组的具体大小未知。但当该模块被下放到目标架构时,该常量值就已知了。
术语
指令
一个SPIR-V模块与指令的物理布局
一个SPIR-V模块是一单串线性字(word)流。开头的一些字如下表所示:
表1:物理布局的起始字
字序号 | 内容 |
---|---|
0 | 魔术数——0x07230203 |
1 | 版本号。这些字节从高位序到低位序: 0 | 主版本号 | 小版本号 | 0 从而,版本 1.3 的值即为: 0x00'01'03'00 |
2 | 生成器的魔术数。它与生成该模块的工具相关联。其值并不影响任何语义,并且允许是0。使用一个非0值是值得鼓励的,并且可以在Khronos中注册。 |
3 | 边界;这里在本模块中的所有 <id> 确保满足:0 < id < Bound 边界应当尽量小,越小越好。一个模块中的所有 <id> 应当密集地打包并从而靠近0。 |
4 | 0(为指令模式而保留,如果需要的话) |
5 | 指令流的第一个字,见下表 |
所有剩下的字是一个线性的指令序列。每条指令的字流如下:
表2:指令物理布局
指令字序号 | 内容 |
---|---|
0 | 操作码:高16位是当前指令的字个数。低16位是操作码枚举值。 |
1 | 可选的指令类型 type <id> (是否存在以操作码来决定) |
- | 可选的指令结果 Result <id> (是否存在以操作码来决定) |
- | 操作数1(如果需要) |
- | 操作数2(如果需要) |
… | … |
字个数 - 1 | 操作数 N(N 由字个数减去 1到3个字来确定,这1到3个字用于操作码、指令类型 type <id> 、以及指令结果 Result <id> )。 |
指令是可变长度的,由于具有可选的指令类型 type <id>
和 Result <id>
字,以及可变个数的操作数。
边栏推荐
- golang mqtt/stomp/nats/amqp
- 普通人下场全球贸易,新一轮结构性机会浮出水面
- Distributed base theory
- [groovy] JSON serialization (jsonbuilder builder | generates JSON string with root node name | generates JSON string without root node name)
- cf:H. Maximal AND【位运算练习 + k次操作 + 最大And】
- WGet: command line download tool
- Tcpdump: monitor network traffic
- Zhuhai's waste gas treatment scheme was exposed
- Questions about database: (5) query the barcode, location and reader number of each book in the inventory table
- 关于softmax函数的见解
猜你喜欢
cf:H. Maximal AND【位运算练习 + k次操作 + 最大And】
Four dimensional matrix, flip (including mirror image), rotation, world coordinates and local coordinates
激动人心,2022开放原子全球开源峰会报名火热开启
[groovy] JSON serialization (convert class objects to JSON strings | convert using jsonbuilder | convert using jsonoutput | format JSON strings for output)
DOM introduction
After Luke zettlemoyer, head of meta AI Seattle research | trillion parameters, will the large model continue to grow?
Five challenges of ads-npu chip architecture design
282. Stone consolidation (interval DP)
Recommended areas - ways to explore users' future interests
Keepalive component cache does not take effect
随机推荐
Programmer growth Chapter 9: precautions in real projects
[groovy] compile time meta programming (compile time method interception | method interception in myasttransformation visit method)
General operation method of spot Silver
测试/开发程序员的成长路线,全局思考问题的问题......
The third season of ape table school is about to launch, opening a new vision for developers under the wave of going to sea
Overview of Zhuhai purification laboratory construction details
Daily practice - February 13, 2022
ADS-NPU芯片架构设计的五大挑战
黄金价格走势k线图如何看?
视频直播源码,实现本地存储搜索历史记录
cf:D. Insert a Progression【关于数组中的插入 + 绝对值的性质 + 贪心一头一尾最值】
Beginner redis
Unity | 实现面部驱动的两种方式
关于#数据库#的问题:(5)查询库存表中每本书的条码、位置和借阅的读者编号
MIT doctoral thesis | robust and reliable intelligent system using neural symbol learning
Cglib dynamic agent -- example / principle
Cannot resolve symbol error
普通人下场全球贸易,新一轮结构性机会浮出水面
Starting from 1.5, build a micro Service Framework - call chain tracking traceid
FFT learning notes (I think it is detailed)