一、概述
NIR(New Intermediate Representation)是 Mesa 3D 图形库中的一种中间表示(IR),用于在图形编译器中表示图形着色器等 GPU 程序的中间形式。
NIR 的设计目标是提供一种灵活、可扩展的中间表示,以便在编译器的不同阶段进行优化和代码生成。它主要用于表示图形着色器程序,包括顶点着色器、片段着色器等。
NIR 的特点包括:
- 灵活性: NIR 提供了一种灵活的表示形式,能够容纳各种类型的 GPU 程序。
- 中间形式: NIR 旨在作为编译器的中间表示,方便在不同编译阶段进行优化。
- 目标中立: NIR 设计为与目标硬件无关,这使得可以在多个图形硬件架构上使用相同的 NIR 表示进行优化和代码生成。
- 易于扩展: NIR 提供了一种扩展机制,可以轻松添加新的操作和类型。
二、基本概念
- 基本块(Basic Block): NIR 使用基本块来组织代码。一个基本块是一段顺序执行的代码,其中没有分支、跳转或返回语句。基本块是 NIR 中的基本执行单元。
- 指令(Instruction): NIR 中的指令表示对数据的操作,类似于汇编语言中的指令。每个指令执行一个特定的操作,例如算术运算、加载存储、条件分支等。指令包含操作码(Opcode)和操作数(Operands)。
- 操作码(Opcode): 操作码是指令的标识符,表示指令要执行的具体操作。例如,算术运算指令的操作码可能是 ADD、SUB,条件分支指令的操作码可能是 IF、ELSE。
- 操作数(Operands): 操作数是指令的输入和输出。输入操作数表示指令要操作的数据,输出操作数表示指令的结果。操作数可以是寄存器、常量或者是其他指令的结果。
- 寄存器(Register): NIR 使用寄存器来存储临时变量和计算结果。寄存器可以是标量、矢量或者其他复杂类型。寄存器的生命周期在基本块内部。
- 表达式(Expression): NIR 使用表达式来表示计算过程。表达式由操作码和操作数组成,描述了一个计算的过程。表达式可以包含多个嵌套的子表达式。
- 控制流图(Control Flow Graph,CFG): 控制流图是由基本块和控制流边组成的图,表示程序中的控制流结构。CFG 描述了基本块之间的执行顺序和条件分支关系。
- 模块(Module): NIR 中的模块是整个着色器程序的表示。模块包含了所有的基本块和全局信息,是 NIR 的最顶层结构。
三、CFG
NIR(New Intermediate Representation)中的 CFG(Control Flow Graph)是表示程序控制流的数据结构。CFG 是由基本块(Basic Block)和控制流边(Control Flow Edge)组成的图,用于描述程序中的顺序、条件分支和循环等控制结构。
CFG 是在编译器优化和分析中广泛使用的数据结构。它提供了对程序控制流结构的抽象,允许编译器对程序进行各种优化,如死代码删除、循环展开等。CFG 还用于执行静态分析,识别程序中的控制流特征,为优化提供更多信息。
控制流结构
在编程中,控制流结构主要有三种形式:顺序结构、选择结构和循环结构。
顺序结构(Sequential Structure): 顺序结构是最基本的结构,指程序按照代码的书写顺序一步一步地执行。每条语句按照顺序执行,没有分支或跳转。大多数程序的执行都包含顺序结构。
选择结构(Selection Structure): 选择结构表示程序在执行过程中根据条件选择不同的执行路径。常见的选择结构包括 if 语句和 switch 语句。
循环结构(Loop Structure): 循环结构表示程序可以重复执行某一段代码,直到满足退出条件。常见的循环结构包括 for 循环、while 循环和 do-while 循环。
控制流图基本结构
在 NIR(New Intermediate Representation)中,控制流图由多个基本块(Basic Block)组成,基本块之间通过控制流边连接。
每个基本块包含一系列按顺序执行的指令,而控制流边表示基本块之间的跳转关系。基本块中的最后一条指令通常确定了下一个要执行的基本块,形成了控制流图的边。
以下是控制流图的基本结构:
- 基本块(Basic Block): 是控制流图的基本执行单元,包含一系列按顺序执行的指令。基本块之间通过控制流边连接。
- 控制流边(Control Flow Edge): 连接两个基本块的边,表示程序执行时的跳转关系。控制流边通常包含一个条件,决定执行哪个基本块。
- 控制流图(Control Flow Graph): 由多个基本块和控制流边组成的图,表示程序的控制流结构。控制流图可以是有向图,其中节点是基本块,边是控制流边。
- 入口基本块(Entry Block): 是控制流图中的起始基本块,表示程序的入口点。
- 出口基本块(Exit Block): 是控制流图中的结束基本块,表示程序的出口点。
- 循环(Loop): 通过控制流图中的循环结构表示。循环由头部基本块和循环体组成。
通过分析控制流图,编译器可以进行各种优化,例如循环展开、循环合并、条件分支优化等,以提高着色器程序的性能和效率。
一些不同的数据类型: - 控制流节点(通常缩写为"cf node"并在 nir.h 中定义为 nir_cf_node)是控制流树中所有内容的基类。它可以是循环、if 语句或基本块。
- 控制流列表(通常缩写为"cf list")是对应于 GLSL 中的一系列语句的控制流节点列表。它用于表示函数体和循环以及 if 语句的 then 和 else 分支。在 NIR 中,它被实现为侵入式链表。
- if 语句(定义为nir_if)包含 then 和 else 分支的控制流列表以及条件。
- 循环(定义为nir_loop)是无限循环(退出的唯一方法是通过break语句)。它只包含代表主体的控制流列表。
- 除了以前的定义之外,基本块现在是控制流树的叶子。在 NIR 中,基本块在名为nir_block的结构中定义。
最后还有一类指令,叫做"跳转指令",在nir.h中定义为nir_jump_instr,这就是NIR中中断、继续、返回的表示方式,注意"多级中断"和"多级继续",即跳转到目前不支持最内层循环之外的循环,尽管将来可能会支持。每个基本块最多只能有一条跳转指令,并且它必须位于块的末尾。
四、数据结构
NIR(New Intermediate Representation)使用一系列数据结构来表示图形着色器程序。NIR 中的所有基本数据结构都是类型定义的 C 结构,
以下是 NIR 中常见的数据结构:
- nir_shader: 表示整个着色器程序的数据结构。它包含了所有的信息,包括顶层的控制流图(CFG)、全局变量和其他与整个着色器相关的内容。
- nir_function: 表示着色器程序中的一个函数。每个函数由一个基本块的列表组成,其中包含了函数的所有执行路径。
- nir_block: 表示基本块,是一段顺序执行的代码。每个基本块包含一系列的指令。
- nir_instr: 表示一个指令,是 NIR 中最基本的操作单元。每个指令包含一个操作码和一组操作数。
- nir_alu_instr: 表示一个算术运算指令。这包括各种算术运算,如加法、乘法等。
- nir_tex_instr: 表示一个纹理采样指令。包括对纹理进行采样的操作。
- nir_load_const_instr: 表示一个加载常量的指令。这用于将常量值加载到寄存器中。
- nir_ssa_def: 表示 SSA(静态单赋值)寄存器的定义。每个 SSA 寄存器都有一个唯一的定义。
- nir_register: 表示一个寄存器。寄存器用于存储变量和计算结果。
- nir_variable: 表示一个变量,通常对应于着色器中的一个变量或者输入/输出。
- nir_src 和 nir_dest: 分别表示指令的源操作数和目标操作数。这些结构包含了对寄存器或常量的引用。
- nir_if: 表示一个条件分支结构。用于在控制流图中表示条件执行的分支。
- nir_loop: 表示一个循环结构。用于在控制流图中表示循环执行的结构。
数据结构共同构成了 NIR 中间表示的基础,通过这些结构,NIR 提供了对图形着色器程序进行灵活分析和优化的能力。
nir_shader
在 NIR(New Intermediate Representation)中,模块是着色器程序的最顶层表示。nir_shader 结构体表示整个着色器程序,包含了与整个着色器相关的信息,如全局变量、全局常量、所有函数等。
以下是 nir_shader 结构体的一些重要成员: - info : 包含有关着色器的一般信息,例如着色器的类型(顶点着色器、片段着色器等)和目标。
- consts : 一个列表,包含所有全局常量的定义。
- inputs 和 outputs: 列表,包含输入和输出变量的定义。
- uniforms : 列表,包含所有全局 uniform 变量的定义。
- shared : 列表,包含所有 shared 变量的定义。
- num_inputs、num_outputs、num_uniforms、num_shared: 分别表示输入变量的数量、输出变量的数量、uniform 变量的数量和 shared 变量的数量。
- functions : 包含所有函数的列表。
- info->entrypoint : 表示着色器程序的入口函数。
在 nir_shader 的 functions 成员中,每个函数由 nir_function 结构体表示,包含一个基本块的列表,其中定义了函数的执行路径。基本块内包含了 NIR 中的指令,完成具体的计算和操作。
这种层次结构允许 NIR 表示和优化着色器程序中的不同部分,通过分析和修改模块、函数、基本块和指令,编译器可以实现各种优化和转换。
nir_function
在 NIR(New Intermediate Representation)中,nir_function 结构表示一个函数。每个 NIR 着色器程序通常包含一个或多个函数,而每个函数则包含一组基本块(nir_block)和相关的信息。
以下是 nir_function 结构体的主要成员:
c
struct nir_function {
struct exec_node node; /* Node in the shader's function list */
/* the name of the function */
char *name;
/* the function's prototype */
nir_function_prototype *prototype;
/* the function's entrypoint */
nir_block *entry_block;
/* list of blocks that make up this function */
struct exec_list blocks;
/* function-local variables */
struct exec_list local_vars;
/* non-NULL if this is a shader function */
nir_shader *shader;
/* non-NULL if this is a kernel */
nir_kernel *kernel;
};
主要成员说明:
- node : 用于将函数链接到着色器程序的函数列表中。
- name : 函数的名称。
- prototype : 函数的原型信息,包括返回类型、参数列表等。
- entry_block : 函数的入口基本块。
- blocks : 包含构成该函数的所有基本块的列表。
- local_vars : 函数内的局部变量列表。
- shader 和 kernel: 分别表示该函数所属的着色器程序和内核(如果是内核函数)。
函数是 NIR 中的一个重要概念,每个函数包含一组基本块,这些基本块通过控制流边连接,形成函数的控制流图。在 NIR 中,函数可以表示着色器程序的不同阶段,例如顶点着色器、片段着色器等。
nir_block
在 NIR(New Intermediate Representation)中,nir_block 结构体表示控制流图中的基本块(Basic Block)。以下是 nir_block 结构体的主要成员:
c
struct nir_block {
struct exec_node node; /* Node in the shader's block list */
/* list of predecessors (including fallthrough) */
struct exec_list preds;
/* list of successors (including fallthrough) */
struct exec_list succs;
/* index into the shader's block array, for cheap block comparison */
unsigned index;
/* list of all instructions in this block */
struct exec_list instr_list;
/* list of phi instructions */
struct exec_list phi_nodes;
nir_block_start start;
bool is_pre_linked;
bool is_linked;
/* used during CSE to determine if a block's instructions have been visited */
unsigned cse_num;
};
主要成员说明:
- node : 用于将基本块链接到着色器程序的基本块列表中。
- preds 和 succs: 分别表示基本块的前驱和后继基本块的列表。
- index : 在着色器程序的基本块数组中的索引,用于进行基本块的快速比较。
- instr_list : 包含该基本块中所有 NIR 指令的列表。
- phi_nodes : 包含该基本块中所有的 PHI 指令(用于表示控制流合并点的指令)的列表。
- start : 标识基本块的类型,例如是否是循环的起始块等。
- is_pre_linked 和 is_linked: 用于标识基本块是否已经链接。
- cse_num : 用于在公共子表达式消除(CSE)期间确定基本块的指令是否已被访问。
基本块是 NIR 中的关键组件,通过链接多个基本块,形成控制流图(Control Flow Graph,CFG),表示整个着色器程序的控制流结构。
ir_instr
在 NIR(New Intermediate Representation)中,nir_instr 结构表示一个中间代码中的指令。NIR 使用一系列指令来表示着色器程序的中间表示,这些指令包括各种操作,例如算术运算、条件分支、内存访问等。
以下是 nir_instr 结构体的主要成员:
struct nir_instr {
/* used to link instructions together in various forms /
struct exec_node node;
/ the type of this instruction /
nir_instr_type type;
/ the instruction's parent block */
nir_block block;
/ the instruction's parent function */
nir_function_impl impl;
/ per-instruction debug annotations */
struct nir_instr_debug_annotations debug_annotations;
};
主要成员说明: - node : 用于将指令链接到不同的指令列表中,例如基本块内的指令列表。
- type : 指令的类型,表示该指令执行的操作,如算术运算、条件分支等。
- block : 指令所属的基本块。
- impl : 指令所属的函数实现,即该指令在哪个函数中。
- debug_annotations : 用于调试的注解信息。
NIR 中的指令包括各种类型,例如 nir_alu_instr 用于表示算术运算,nir_if 用于表示条件分支,nir_load_var 用于表示变量加载等。每个指令都包含在一个基本块内,通过控制流边连接,形成函数的控制流图。
不同类型的指令执行不同的操作,而它们通过控制流图的连接关系来表示程序的控制流结构。指令是 NIR 中的核心概念,编译器通过对指令进行分析和优化,生成目标代码。
nir_function_impl
在 NIR(New Intermediate Representation)中,nir_function_impl 表示一个函数的具体实现,即函数的定义和实际代码。每个 NIR 函数可以有多个实现,这些实现对应不同的阶段或版本。
以下是 nir_function_impl 结构体的主要成员:
c
struct nir_function_impl {
struct exec_node node; /* Node in the shader's function_impl_list */
nir_function *function;
/* the function's name, if it has one */
char *name;
/* list of locals defined in the function */
struct exec_list locals;
/* list of basic blocks that make up the function */
struct exec_list blocks;
/* entry point to the function; entry_block->cfg[0] is the fallthrough
* path and the rest are conditions.
*/
nir_block *entry_block;
/* the function's return type; if NULL, the function returns void */
const struct glsl_type *return_type;
/* does the function return void? */
bool return_type_is_void;
/* shader this impl belongs to */
nir_shader *shader;
};
主要成员说明:
- node : 用于将函数实现链接到着色器程序的函数实现列表中。
- function : 指向该函数实现所属的函数。
- name : 函数实现的名称。
- locals : 包含在函数内定义的本地变量列表。
- blocks : 包含构成该函数实现的所有基本块的列表。
- entry_block : 函数实现的入口基本块。
- return_type 和 return_type_is_void: 函数实现的返回类型,以及函数是否返回 void。
- shader : 该函数实现所属的着色器程序。
函数实现是 NIR 中表示函数体的具体实现,它包含了一系列基本块,每个基本块包含了一组指令。通过链接基本块和指令,形成了函数的控制流图和具体代码。函数实现是编译器进行分析和优化的重要对象。
nir_ssa_def
nir_ssa_def 是 NIR(New Intermediate Representation)中的一种特殊类型,表示静态单赋值形式(SSA)中的定义。SSA 是一种中间表示形式,其中每个变量只被赋值一次。在 NIR 中,nir_ssa_def 用于表示 SSA 变量的定义。
以下是 nir_ssa_def 的伪代码定义:
struct nir_ssa_def {
nir_instr instr; // 与该 SSA 定义相关联的指令
unsigned index; // SSA 定义的索引
unsigned num_components;// 定义的向量元素数量
nir_dest dest; // 目标寄存器信息
}; - instr 字段是一个指向相关指令的指针,表示该 SSA 定义所在的指令。
- index 字段是 SSA 定义的唯一索引,用于区分不同的 SSA 定义。
- num_components 字段表示定义的向量元素数量,例如,如果是一个 4 维向量,num_components 就是 4。
- dest 字段表示 SSA 定义的目标寄存器信息,包括寄存器类型和寄存器索引等。
在 NIR 中,SSA 定义是指令执行的结果,它们的值在整个程序执行过程中不会改变。SSA 形式有助于进行数据流分析和优化。
在 NIR 编译器中,通过对 nir_ssa_def 进行操作,可以进行各种优化,例如常量传播、死代码消除等,以生成更高效的 GPU 着色器代码。
nir_if
nir_if 是 NIR(New Intermediate Representation)中的结构,用于表示条件分支语句。在 GPU 编程中,条件分支允许根据某个条件选择不同的执行路径。nir_if 结构体用于表示 NIR 中的条件分支。
以下是 nir_if 结构体的伪代码定义:
c
struct nir_if {
nir_instr instr; // 分支指令
nir_src condition; // 分支条件
nir_block *then_list; // 条件为真时执行的块链表
nir_block *else_list; // 条件为假时执行的块链表
nir_block *end_block; // 分支结束后的块
};
- instr 字段是一个指向分支指令的指针,表示该 nir_if 结构与一个特定的指令相关联。
- condition 字段是一个 nir_src 结构,表示条件表达式,它是一个源操作数。
- then_list 和 else_list 字段分别是条件为真和条件为假时执行的块链表。这些链表中的块包含了在不同条件下执行的代码。
- end_block 字段表示条件分支的结束块,即在条件分支执行完毕后跳转到的块。
nir_if 结构体用于表示类似于 C 语言中的 if 语句,允许根据条件选择执行不同的代码块。在 NIR 编译器中,通过对 nir_if 进行分析和优化,可以生成更高效的 GPU 着色器代码,例如通过消除冗余的条件分支和优化条件表达式
nir_loop
nir_loop 是 NIR(New Intermediate Representation)中的结构,用于表示循环语句。在 GPU 编程中,循环允许多次执行同一段代码。nir_loop 结构体用于表示 NIR 中的循环。
以下是 nir_loop 结构体的伪代码定义:
struct nir_loop {
nir_instr instr; // 循环指令
nir_block *body; // 循环体块链表
nir_block *continue_block; // 循环继续的块
nir_block *exit_block; // 循环退出的块
}; - instr 字段是一个指向循环指令的指针,表示该 nir_loop 结构与一个特定的指令相关联。
- body 字段是循环体块链表,其中包含了循环内执行的代码。
- continue_block 字段表示循环继续的块,即在循环迭代完毕后跳转到的块。
- exit_block 字段表示循环退出的块,即在不满足循环条件时跳转到的块。
nir_loop 结构体用于表示类似于 C 语言中的 for、while 或 do-while 循环。在 NIR 编译器中,通过对 nir_loop 进行分析和优化,可以生成更高效的 GPU 着色器代码,例如通过展开循环、消除冗余循环等优化手段。
nir_register
在NIR(New Intermediate Representation)中,nir_register 用于表示寄存器。寄存器是在GPU着色器程序中存储数据的一种方式。以下是 nir_register 结构的伪代码定义:
c
struct nir_register {
nir_dest dest; // 寄存器目的地信息
unsigned num_components; // 寄存器包含的分量数量
unsigned bit_size; // 每个分量的位数
unsigned num_array_elems; // 寄存器数组元素的数量
bool is_packed; // 是否为紧凑格式
bool is_scalar; // 是否为标量
bool is_array; // 是否为数组
bool is_virtual; // 是否为虚拟寄存器
bool is_file_temp; // 是否为临时寄存器
bool is_global; // 是否为全局寄存器
bool is_system_value; // 是否为系统值寄存器
bool is_block_local; // 是否为块局部寄存器
bool is_intrinsic; // 是否为内建寄存器
unsigned index; // 寄存器索引
nir_type type; // 寄存器数据类型
};
- dest 字段表示寄存器的目的地信息,包括寄存器类型和寄存器索引等。
- num_components 字段表示寄存器包含的分量数量,例如,一个4维向量寄存器的 num_components 为 4。
- bit_size 字段表示每个分量的位数,即寄存器的位宽。
- num_array_elems 字段表示寄存器数组元素的数量,如果寄存器不是数组,则为 1。
- is_packed 表示寄存器是否以紧凑格式存储。
- is_scalar 表示寄存器是否为标量。
- is_array 表示寄存器是否为数组。
- is_virtual 表示寄存器是否为虚拟寄存器。
- is_file_temp 表示寄存器是否为临时寄存器。
- is_global 表示寄存器是否为全局寄存器。
- is_system_value 表示寄存器是否为系统值寄存器。
- is_block_local 表示寄存器是否为块局部寄存器。
- is_intrinsic 表示寄存器是否为内建寄存器。
- index 表示寄存器的索引。
- type 表示寄存器的数据类型,例如,float、int、uint 等。
nir_register 结构体用于在NIR中表示寄存器,它包含了寄存器的各种属性,这些属性对于NIR编译器来说是关键的信息。
NIR 包含一个名为nir_print_shader()的函数,用于将着色器的内容打印到给定的FILE *。此外,还公开了nir_print_instr(),这对于检查调试器中的指令很有用。
五、指令
ALU
ALU 指令表示简单的运算,例如加法、乘法、比较等,它们采用一定数量的参数并返回仅取决于参数的结果。一个好的经验法则是,只有可以常量折叠的东西才应该是 ALU 运算。如果它不能被常量折叠,那么它可能应该是一个内在的。
每个 ALU 指令都有一个操作码,它是枚举 ( nir_op ) 的成员,描述它的作用以及它需要多少个参数。与每个操作码相关联的是一个信息结构(nir_op_info),它显示操作码采用多少个参数以及操作码是否可交换或关联,
内部指令
两个特别重要的内在函数是load_var和store_var,所有变量的加载和存储都通过它们进行。大多数对变量的访问(除了对纹理和缓冲区的访问之外)都是通过核心 NIR 中的这些指令进行的,尽管它们可以在到达后端之前使用实际索引降低到加载/存储到寄存器、输入、输出等。
内在操作码是在头文件 nir_intrinsics.h 中定义的,该文件扩展为一系列INTRINSIC 宏。 nir_intrinsics.h 包含两次,一次在 nir.h 中创建 nir_intrinsic_op,另一次在nir_intrinsics.c中创建 nir_intrinsic_infos数组。
回调指令
NIR 中的调用指令非常简单。它们包含指向它们引用的重载的指针。参数通过解引用传递,解引用可以被复制、复制到或两者都复制,具体取决于重载中的匹配参数是输入和/或输出。
跳转指令
NIR 中的跳转指令是中断、继续或返回。返回值不包含值;相反,返回值的函数会填充一个专门指定的变量,即返回变量。
纹理指令
NIR 有专用的纹理指令。仍然有一个源数组,只不过每个源还有一个与之关联的类型。源类型有多种,每种类型对应不同纹理操作所需的一条信息。每种类型最多可以有一个源。
常量加载指令
该指令创建一个常量的 SSA 值
Undef指令
创建未定义的 SSA 值。
Phi指令
当某个前驱块分支到具有 phi 节点的块时,该前驱块对应的源被复制到 phi 节点的目的地。如果一个块中有多个 phi 节点,那么这一过程会并行发生。 Phi 节点必须位于块的开头,即每个块必须包含任何 phi 指令,后跟任何非 phi 节点。
六、变量
后端独立性的主要机制之一是通过 变量,它基于GLSL变量(实现主要取自GLSL IR)。变量是逻辑变量而不是物理变量,这意味着对它们的所有访问都不会意识到任何布局问题(即使在某些情况下,例如 UBO,API 已经定义了布局),并且它们可能不包含任何指针; NIR 甚至没有指针的概念。
解除引用
在 NIR(Nouveau Intermediate Representation)转换中,解除引用(Dereferencing)是一个关键操作,特别是在处理变量、指针、数组、结构体等复杂类型时。
解除引用指的是访问变量或数据结构中的实际存储位置或成员值的过程。在高级编程语言中,变量可以是指针、数组、结构体等复杂类型,通过解除引用可以获取它们指向或包含的实际数据。
在 NIR 转换过程中,当遇到 GLSL 的复杂类型,如数组、指针、结构体等,必须将它们的抽象表示转换为 NIR 的低层次操作。这涉及到:
- 从指针或引用类型中访问具体的值。
- 从数组或结构体中访问具体的元素或成员。
- 处理内存偏移、地址计算和寄存器访问等低层次操作。
在 NIR 中,解除引用通过deref(dereference)操作表示,通常用 nir_deref_var、nir_deref_struct、nir_deref_array 等不同的类型表示不同的解除引用操作。这些指令主要负责解析变量、数组、结构体等的实际内存地址或寄存器位置。
示例:NIR 中的解除引用
让我们通过具体的例子来说明 NIR 中解除引用的工作原理。
GLSL 代码示例
假设有以下 GLSL 代码:
c
struct Light {
vec3 position;
vec3 color;
};
uniform Light lights[4];
void main() {
vec3 lightPos = lights[1].position;
}
在这个例子中,我们定义了一个 Light 结构体数组,并且从数组中访问了第二个元素的 position 成员。这涉及到多个解除引用操作:首先解除对数组的引用,然后解除对结构体成员的引用。
NIR 中的解除引用
在 GLSL 到 NIR 的转换过程中,类似的代码会被转换为 NIR 中的解除引用操作。对应的 NIR 表示大概如下:
deref_var &lights; // 对全局变量 lights 的解除引用
deref_array &lights[1]; // 对数组中索引为 1 的元素的解除引用
deref_struct &lights[1].position; // 对结构体中成员 position 的解除引用
这里的每个 deref 操作都代表解除引用的过程:
- deref_var &lights:解除对 lights 这个全局变量的引用,表示我们正在访问一个变量(数组 lights)。
- deref_array &lights[1]:解除对数组 lights 索引为 1 的元素的引用,表示我们正在访问 lights 数组中的第 2 个元素(索引从 0 开始)。
- deref_struct &lights[1].position:解除对结构体 Light 的 position 成员的引用,表示我们正在访问该结构体实例的 position 成员。
变量与寄存器、输入/输出
变量是处理大多数非 SSA 值的事物的核心 NIR 方式,后端直接使用地址和位置,将变量取消引用转换为涉及一些工作并创建需要清理的代码。因此,NIR 中有各种机制可以替代变量的大部分使用,并允许驱动程序的降级Pass,将变量引用转换为对平面地址空间的引用,包括:
- 几乎所有可以使用 SSA 值的地方都可以使用寄存器(实际上是虚拟寄存器)。这意味着它们可以用作普通局部或全局变量(即只能加载或存储的每个实例变量)的替代品,也可以用作使 NIR 着色器几乎完全摆脱 SSA 的方法。
- 对于输入、输出和统一,存在加载/存储内在函数,它们采用加在一起的直接(常量)和间接索引。
- 对于纹理,类似地,也有地方可以为采样器添加与变量互补的直接和间接索引。
七、GLSL IR to NIR
glsl_to_nir():将 GLSL IR 代码转换为 NIR 代码。此时,程序中的所有信息现在都在 NIR 数据结构中,Pass生成的代码glsl_to_nir采用 SSA 形式。但是,它包含许多我们最终希望消除的变量加载/存储内在函数。
c
nir_shader *
glsl_to_nir(const struct gl_constants *consts,
struct exec_list **ir, shader_info *si, gl_shader_stage stage,
const nir_shader_compiler_options *options)
{
MESA_TRACE_FUNC();
nir_shader *shader = nir_shader_create(NULL, stage, options, si);
nir_visitor v1(consts, shader);
nir_function_visitor v2(&v1);
v2.run(*ir);
visit_exec_list(*ir, &v1);
/* The GLSL IR won't be needed anymore. */
ralloc_free(*ir);
*ir = NULL;
nir_validate_shader(shader, "after glsl to nir, before function inline");
if (should_print_nir(shader)) {
printf("glsl_to_nir\n");
nir_print_shader(shader, stdout);
}
shader->info.subgroup_size = SUBGROUP_SIZE_UNIFORM;
return shader;
}
nir_function_visitor 和 visit_exec_list 实际上是遍历 GLSL IR 的执行列表和函数,并将它们转换为 NIR 表示。些访问器不仅是在转变数据结构,实际上是执行了细节的转换,
-
nir_lower_global_vars_to_local:对于每个全局变量,它会查看该变量在着色器中每个函数实现中的所有使用情况。如果它仅在一个函数实现中使用,则它会被降级为局部变量。
-
nir_split_var_copies:实现"复制拆分",与结构拆分类似,只是它适用于复制操作而不是数据类型本身。 GLSL 语言允许您一次将一个变量复制到另一个整个结构(可能包含数组或其他结构)。
-
优化循环:在循环中执行一系列不同的优化。每个优化过程都会返回一个布尔值来指示它是否取得了任何"进展"。循环继续重复,直到完成整个循环而没有任何"进展"。我不会逐一进行优化,但其中一些关键的优化如下:
- nir_lower_variables():此过程(可能需要更好的名称)尽可能将变量加载/存储内在函数降低为 SSA 值。特别是,它分析特定值是否曾经被间接别名别名,如果没有,则将其降低为 SSA 值,并在必要时插入 phi 节点。
- nir_opt_algebraic():此通道能够对表达式进行各种代数简化。一个简单的例子是a + 0 -> a。一个更复杂的示例是, (-|a| > 0) -> (a == 0)它始终出现在由 DirectX 到 OpenGL 转换层生成的 GLSL 代码中。
- nir_opt_constant_folding():查找参数为常量的 ALU 运算,计算结果常量值,并用单个 load_const 指令替换表达式。常量折叠传递还能够将常量折叠为变量间接引用。这样,在不断的折叠过程之后,曾经是间接变量取消引用的东西现在是直接的,我们可以将它们降低到 SSA。
低级处理:这些较低的 NIR 概念使后端编译器更容易处理。 i965后端使用的有: - nir_lower_locals_to_regs():此过程将局部变量降低为nir_register值。同样,这使后端不必担心解引用链。
- 取消引用降低。这些Pass基本上都做相同的事情:将使用变量解引用链的指令降低到不使用变量解引用链的指令。这样,后端就不必担心抓取取消引用,并且可以仅使用缓冲区索引和偶尔的间接偏移。这些Pass包括:
- nir_lower_io()
- nir_lower_samplers()
- nir_lower_system_values()
- nir_lower_atomics()
- nir_lower_to_source_mods():因为它使代数优化更容易,所以我们实际上并不在 中发出源或目标修饰符glsl_to_nir。相反,我们发出 [fi]neg、[fi]abs和fsatALU 指令并在优化期间使用这些指令。完成所有优化后,我们将这些降低到源/目标修饰符。
- nir_convert_from_ssa():此通道将着色器从 SSA 形式转变为更传统的形式,使用 nir_register.一般来说,摆脱 SSA 形式很难做得正确。如果其他后端不想成为 SSA,我们宁愿在一处实现 SSA 之外的实现,这样他们就不会出错。
八、SSA处理
在 NIR 中,有两种方法可以进入 SSA 形式。一种是首先转换为使用寄存器并调用nir_to_ssa().另一个是我们在 i965 中所做的,我们生成 SSA+load/store,然后调用" nir_lower_variables()我将遍历整个 nir_lower_variables()过程",因为它是两者中更复杂的一个,但它们都遵循相同的算法来放置 phi 节点等。
- 收集有关所有变量取消引用的信息。这构建了一个我称之为"解引用森林"的数据结构,其中包含有关每个可能的变量解引用及其使用位置的信息。这类似于标量寄存器的 use/def 信息,但是在每个 deref 的基础上完成的,以便您可以区分结构的不同元素或数组中的不同偏移量。
- 确定哪些值可以降低到 SSA 值。目前,这是一个简单的启发式方法,只有在永远无法间接引用的情况下才会降低值。德雷夫森林让你很容易找到这些。
- 将 phi 节点放置在定义的迭代主导边界处。这遵循 Cytron 等人提出的算法。等人。在"有效计算静态单赋值形式和控制依赖图"中。它将 phi 节点放置在需要它们的最少位置,以便解析 CFG 中的合并点。
- 变量重新编号。这是一个遍历指令列表的过程,并用 SSA 定义替换每个存储操作,并用到达该指令的最接近的 SSA 定义的区域替换每个加载操作。由于我们已经添加了 Phi 节点,因此可以通过简单的基于堆栈的深度优先搜索算法来完成。
在nir_to_ssa()传递中,我们可以插入 phi 节点,其中所有源和目的地都是我们尝试采用 SSA 形式的寄存器。因为我们不能使用变量作为 phi 节点的源或目的地,所以我们在传递 nir_lower_variables()中用哈希表来伪造它,但原理是相同的。