Vulkan编程指南翻译 第六章 着色器和管线 第2节 SPIR-V 概述

6.2  SPIR-V 概述

 

SPIR-V着色器嵌入在module里。每一个module都可以包含一个或多个着色器。每一个着色器都有一个固定名字入口点和着色器类型,这用来定义着色器在哪个着色阶段来使用。入口点之着色器开始运行时的起始位置。一个SPIR-Vmodule都随着一些附带信息传递 给VulkanVulkan返回一个对象来表示这个module。此module可以用来构造一个管线,管线是附带信息的着色器经过编译的版本,可以在设备上运行。

 

6.2.1  如何表示SPIR-V

SPIR-VVulkan官方唯一支持的着色语言。在API层面被Vulkan接受,最终用来构建管线,管线对象可配置一个设备来完成应用程序的工作。

SPIR-V被设计为可被工具和驱动很简单就能使用。这减少了不同实现之间的不同意提高兼容性。SPIR-V module在内存的32bi每字数据流形式存储的。除非你是工具作者或者计划自己生成SPIR-V,你不大可能会去直接处理二进制编码的SPIR-V。相反,你要么会去看SPIR-V的文本形式,或者使用诸如glslangvalidatorChronos官方GLSL编译器)的工具来生成SPIR-V

把着色器程序以.comp后缀名的方式保存为文本文件,告诉glslangvalidator把它当作计算着色器编译。我们可以通过使用glslangvalidator命令行的方式来编译着色器:

glslangvalidator simple.comp -o simple.spv

这会产生一个名为simple.spv SPIR-V二进制文件。我们可以使用SPIR-V反汇编工具,spirv-dis,来反编译这个二进制文件,会输出一份人可读的反汇编代码。如Listing 6.2所示:

Listing 6.2: Simplest SPIR-V

; SPIR-V

; Version: 1.0

; Generator: Khronos Glslang Reference Front End; 1

; Bound: 6

; Schema: 0

OpCapability Shader

%1 = OpExtInstImport "GLSL.std.450"

OpMemoryModel Logical GLSL450

OpEntryPoint GLCompute %4 "main"

OpExecutionMode %4 LocalSize 1 1 1

OpSource GLSL 450

OpName %4 "main"

%2 = OpTypeVoid

%3 = OpTypeFunction %2

%4 = OpFunction %2 None %3

%5 = OpLabel

OpReturn

OpFunctionEnd

 

你可以看到SPIR-V的文本形式看起来像汇编语言的变体。我们可深入这段反汇编,来看看他和原始的输入文件如何关联。反汇编文件的每一行待变一个SPIR-V指令,可能由多个token组成。

第一个指令OpCapability Shader,要求这个着色器开启兼容模式。SPIR-V功能粗略的分为“指令”和“特征”。在你的着色器程序可以使用这些特征之前,shader必须要声明使用的特征是哪一部分的。Listing 6.2的着色器程序是一个图形着色器,因此使用Shader能力。这是最基础的能力了。没有这个,我们不能编译图形着色器。随着我们引入更多SPIR-VVulkan功能,我们将介绍这些不同能力所依赖哦特征。

下一行,我们看到 %1 = OpExtInstImport "GLSL.std.450"。 这就是引入了一些附加的指令,对应着GLSL 450包含的功能,这也是原始着色器程序所写入的版本。注意这个指令以 %1 = 开头。这把指令计算的结果使用ID命名。OpExtInstImport的结果是一个有效的库。当我们需要调用这个库的函数,我们可以使用OpExtInst指令,它接受一个库(OpExtInstImport指令的结果)和一个指令索引。这允许SPIR-指令集被任意的拓展。

下一行,我们看到一些附加的声明,OpMemoryModel指定了这个module的工作内存的模型,这是对应GLSL 450的逻辑上的内存模型。这意味着所有的内存访问是通过资源而不是通过指针访问内存的物理内存模型。

下一行是这个module入口点的声明。OpEntryPoint GLCompute %4 "main" 指令表明对应的OpenGL计算着色器有一个可用的入口,以函数名字main导出为ID 4。这个名字被用来当把结果着色器module返回Vulkan时的参考入口点。

我们在后续的指令中使用这个IDOpExecutionMode %4 LocalSize 1 1 1, 它定义了这个着色器的工作组大小为 1 × 1 × 1 work item。 如果没有局部大小的layout限定符,这在GLSL中是隐式的。

下面两个指令是简单的声明类型的。OpSource GLSL 450表示这个moduleGLSL 450 版本编译而来,OpName 4 "main"提供了ID4token的名字。

现在,我们看到这个函数的真身了。第一, %2 = OpTypeVoid声明了我们想要以void类型使用ID 2. 所有东西在SPIR-V中都有一个ID,甚至是类型声明。大型、复合的类型可以通过连续的小的,简单的类型组成。然而,我们需要从某处开始,并且指定我们开始的地方类型为void

%3 = OpTypeFunction %2便是我们定义了ID 3 为一个函数类型,类型为void,不接受参数。我们在下一行使用它,%4 = OpFunction %2 None %3。这表示我们声明ID 4(知其那被命名为“main”)为函数3的一个实例(在上一行声明),返回void(如ID 2),且没有特殊的声明,这通过指令中的None表明,而且可以被用来表示诸如inline,不管变量是否为常量(常量性)等等。

最后,我们可以看到一个label(没有被用到,编译器操作的副作用)的声明,隐式的返回语句,和函数的结束。这是我们SPIR-Vmodule的结尾。

这个着色器程序的二进制dump192字节长。SPIR-V是非常详尽的,因为192字节比原来的着色器要长。然而,SPIR-V把原来的着色语言中隐式的东西变得显式了。比如,在GLSL中无需声明内存模型,因为它只支持一种逻辑内存模型。更有,这里编译的SPIR-V module有一些冗余信息,我们不关心main函数的名字,ID 5 label从来没有被使用,且着色器引入了GLSL.std.450库,但是从来没有使用过。我们可以把这个module中多余的信息抽离出去。即使在此之后,因为SPIR-V编码方式相对稀疏,产生的二进制文件使用一个通用的压缩器也相当容易被压缩,而且使用一个专门的压缩库可以把它压缩的更紧致 。

所有的SPIR-V编码都通过SSA来书写(single static assignment),这表明每一个虚拟寄存器(在上面的List中写作%ntoken)都被仅写入一次。几乎所有的工作的指令产生一个结果标识符。当我们开始写更复杂的着色器时,你将看到机器离线生成的SPIR-V有点笨拙,因为它的详尽特性和SSA形式,非常难以手写。非常建议你在应用程序中以lib形式使用编译器来离线生成SPIR-V

如果你计划自己生成或者解释SPIR-V module,你可以使用预定义的二进制编码器来构建工具,来解析或生成它们。然而,它们都有格式良好的二进制存储格式,我们在本章稍后讲解。

所有的SPIR-V module文件都以一个magic number开始,这可以用来简单的炎症二进制块,实际上就是SPIR-V module。这个魔法数字以无符号整型来看是0x07230203。这个数字也可以用来退单module的字节顺序。因为每一个SPIR-V toke 都是一个32-bit word,如果一个SPIR-V module通过磁盘或网络传输到有不同字节顺序的主机端,这些在一个word内的字节都被交换位置了,它的值给改变了。例如,如贵哦一个SPIR-V module 存储为小端格式,被加载到一个大端格式主机CPU,那么魔法书记就会被读为0x03022307,所以这个CPU就知道需要在这个module里交换字节顺序。

在魔法数字的后面有几个word,描述了module的属性。第一个是SPIR-V使用的的版本数字。它被编码在一个32-bit word里,其中16-23 bit为包含主版本号,8-15 包含次版本号。 SPIR-V 1.0 因此使用编码0x00010000。版本号剩下的bit位是保留的。下一个token包含生成SPIR-V module的工具的版本号。这个值由工具自定义。

下一个是本module中最大的ID号码。所有的变量,函数,和SPIR-V module的其他成员都被赋值到一个比这个数字小的ID,所以,在最前面包含这个数字允许工具分配好数组来存放它们,而不是随时的分配内存。头部最后一个word是保留的,应被置为0。接下来的是指令流。

 

6.2.2  SPIR-V传递给Vulkan

Vulkan并不关心SPIR-V 着色器和module从哪里来。通常,它们会被离线编译好作为应用程序的一部分,或者在应用程序中直接生成。一旦你有了一个SPIR-V module,你需要把它传递给Vulkan,以便可以使用它创建一个着色器module 对象。可以调用vkCreateShaderModule()来做到,其原型如下:

VkResult vkCreateShaderModule (

VkDevice device,

const VkShaderModuleCreateInfo* pCreateInfo,

const VkAllocationCallbacks* pAllocator,

VkShaderModule* pShaderModule);

和所有的Vulkan对象创建函数一样,vkCreateShaderModule()接受一个设备handle输入,和一个指向包含创建对象信息的数据的指针。此时,就是VkShaderModuleCreateInfo类型,其定义为:

typedef struct VkShaderModuleCreateInfo {

VkStructureType sType;

const void* pNext;

VkShaderModuleCreateFlags flags;

size_t codeSize;

const uint32_t* pCode;

} VkShaderModuleCreateInfo;

VkShaderModuleCreateInfosType域应置为VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFOpNext应置为nullptrflags域保留使用应置为0codeSize域包含了SPIR-V module字节单位的大小,代码通过pCode传入。

如果这个SPIR-V代码是有效的,且能够被Vulkan理解,那么vkCreateShaderModule()将返回VK_SUCCESS,并

你可能感兴趣的:(笔记,Vulkan专栏,VR,图形,OpenGL)