OpenGL程序管线对象 Program Pipeline Objects

OpenGL程序管线对象 Program Pipeline Objects

在OpenGL中,为了实现更灵活的着色器组合和管理,可以将不同类型的着色器阶段分别封装到多个程序对象中,而不是全部打包在一个单一的程序对象内。一个程序对象可能仅包含一个或部分完整的管线所需的着色器阶段,这样就可以根据需要以各种方式组合不同的着色器,而无需为每种组合创建一个新的程序对象。

程序管线对象(program pipeline object)是用来容纳所有着色器类型与各自对应程序对象绑定关系的容器。通过这种方式,可以指定每个着色器阶段(如顶点着色器、片段着色器等)所对应的程序对象。

  1. 无当前着色器程序时
    如果没有通过 glUseProgram 函数设置当前的着色器程序,那么渲染管线会自动从已绑定的程序管线对象(如果有)获取各个着色器阶段所使用的着色器程序和 uniform 更新的相关信息。这意味着在没有明确指定单个着色器程序的情况下,整个渲染流程将按照程序管线中预配置的状态进行。

  2. 有当前着色器程序时
    当通过 glUseProgram 设置了一个当前的着色器程序,那么无论是否有绑定的程序管线对象,渲染过程以及 uniform 更新都将只根据这个当前激活的着色器程序来执行。此时,即使存在已绑定的管线对象,它对渲染行为和 uniform 更新也无效。

  3. 使用绑定的程序管线对象渲染
    当确实要利用一个已绑定的程序管线对象进行渲染时,管线中的各个着色器阶段(如顶点着色器、几何着色器、片段着色器等)对应的可执行代码将分别从管线内关联的各个独立着色器程序中提取出来。这一点与直接使用 glUseProgram 指定单一着色器程序不同,管线允许组合多个着色器程序,并且可以预设它们之间的状态切换关系,以实现更灵活高效的渲染流程管理。在OpenGL规范的7.3章节关于 glUseProgram 的讨论中对此有详细说明。


void glGenProgramPipelines( sizei n, uint *pipelines );

生成并返回未使用的n个程序管线对象名称,并将其存储在 pipelines 指向的数组中。这些新生成的名称将被标记为已使用,但它们只有在首次绑定时才会获得具体的状态信息。


void glDeleteProgramPipelines( sizei n, const uint *pipelines );

当调用此函数时,OpenGL 将删除由 pipelines 数组中指定的 n 个程序管线对象。


boolean glIsProgramPipeline( uint pipeline );

检查一个给定的名称是否代表一个有效的程序管线对象。


void glBindProgramPipeline( uint pipeline );

将之前生成的名称与管线对象关联起来,从而创建一个新的状态向量。

  • 当 pipeline 设置为通过 glGenProgramPipelines 生成的新名称时,会创建一个新的、初始状态的管线对象。
  • 如果 pipeline 是已经存在的管线对象名称,则会绑定到该对象,并断开对任何先前所绑定管线对象的引用。
  • 如果 pipeline 设置为零,则解除当前的程序管线绑定,这意味着此时没有“当前”管线对象。

void glCreateProgramPipelines( sizei n, uint *pipelines );

这个函数的作用是:

  • 创建并初始化 n 个新的程序管线对象。
  • 将新创建的管线对象的名称存储在 pipelines 指向的数组中。
  • 新创建的每个管线对象都是一个状态向量,其包含的所有状态都将进行初始化。

这意味着,与 glGenProgramPipelines 和后续绑定操作相比,glCreateProgramPipelines 提供了一步到位的创建和初始化过程,无需额外的绑定步骤即可获得已准备就绪的程序管线对象。


void glUseProgramStages(uint pipeline, GLbitfield stages, GLuint program);

用于将程序对象关联的各个着色器阶段的可执行代码集成到程序管线状态中。

  • pipeline 是要更新的程序管线对象。
  • stages 是接受的常量的按位 OR,代表着各种着色器阶段。
  • program 是要从中获取可执行文件的程序对象标识符。

stages 参数中设置的位表示了要使得由 program 标识的程序对象成为当前状态的着色器阶段。这些阶段可以包括计算着色器、顶点着色器、曲面细分控制着色器、曲面细分评估着色器、几何着色器或片段着色器,分别用 COMPUTE_SHADER_BITVERTEX_SHADER_BITTESS_CONTROL_SHADER_BITTESS_EVALUATION_SHADER_BITGEOMETRY_SHADER_BITFRAGMENT_SHADER_BIT 来表示。常量 ALL_SHADER_BITS 表示要使得 program 成为所有着色器阶段的当前状态。

如果 program 是指向具有有效着色器的程序对象的引用,则此调用会将该阶段的可执行代码安装到指定的程序管线对象状态中。如果 glUseProgramStages 使用 program 设置为零,或者使用一个对于 stages 中的任何阶段都没有可执行代码的程序对象,那么就好像该管线对象未配置任何可编程阶段一样。

如果 pipeline 是由 glGenProgramPipelines 生成的名称(而没有后续删除),但是尚未绑定到先前生成的程序管线对象,则 OpenGL 会首先以与 glBindProgramPipeline 创建新程序管线对象相同的方式创建新的状态向量。


void glActiveShaderProgram( uint pipeline, uint program );

用于在程序管线(Program Pipeline)上下文中激活一个已链接的着色器程序。

这个函数的作用是将名为 program 的已链接程序设置为活动程序,用于统一变量更新,以供程序管线对象 pipeline 使用。

如果 program 为零,那么就表示该管线对象没有活动程序。

如果 pipeline 是由 glGenProgramPipelines 生成的名称,并且尚未绑定到之前创建的程序管线对象,则 OpenGL 会首先以类似于 glBindProgramPipeline 创建新程序管线对象的方式创建新的状态向量。

色器程序对象的更新规则:

  1. 如果通过 glUseProgram 绑定了非零的程序对象,则该对象是活动程序对象,其统一变量由相应的命令进行更新。
  2. 如果没有使用 glUseProgram 绑定程序对象,则活动程序对象是由 glActiveShaderProgram 设置的当前程序管线对象的活动程序对象。
  3. 如果当前程序管线对象没有活动程序对象,或者没有当前程序管线对象,则没有活动程序对象。

在多个着色器阶段同时活动时,一个阶段的输出会与下一个阶段的输入形成接口。在每个这样的接口处,着色器的输入会被匹配到前一阶段的输出:

  • 输出块如果与后续着色器中的输入块具有相同的块名称,并且块成员在名称、类型、限定符和声明顺序上完全一致,则视为匹配。在着色器接口匹配过程中,忽略内建声明的 gl_PerVertex 着色器接口块的 gl_PointSize 成员。

  • 名称不匹配但具有位置并按上述其他方式匹配的输出块,在某些实现中可能被视为匹配,但在所有实现中并不保证如此,因此不应依赖此行为。

  • 输出变量如果满足以下条件之一,则被认为与后续着色器中的输入变量匹配:(1)两者在名称、类型和限定符上相匹配,且都没有位置限定符;或(2)两者都使用相同的位置和组件布局限定符声明,并在类型和限定符上匹配。

    对于带有位置布局限定符但没有组件布局限定符声明的变量,默认认为它们声明了一个组件布局为零的限定符。声明为结构体类型的变量或块成员仅当其结构体成员在名称、类型、限定符和声明顺序上完全匹配时,才被视为类型匹配。声明为数组的变量或块成员仅在两个声明都指定了相同元素类型和数组尺寸时,才被视为类型匹配。

  • 细分控制着色器每顶点输出变量和块以及细分控制、细分评估和几何着色器每顶点输入变量和块必须声明为数组,数组中的每个元素代表多顶点基本图元中单个顶点的输入或输出值。在接口匹配时,这些变量和块被当作未声明为数组来处理。

  • 当程序对象包含多个着色器时,LinkProgram 将检查各着色器阶段间接口是否存在不匹配,并在检测到不匹配时生成链接错误。如果任何静态引用的输入变量或块没有匹配的输出,则会产生链接错误。若任一着色器重新声明了内置数组 gl_ClipDistance[] 或 gl_CullDistance[],则这两个数组在所有着色器中必须具有相同的大小。

  • 使用可分离程序对象时,着色器阶段间的接口可能涉及来自一个程序对象的输出和另一个程序对象的输入。由于这些程序是分开链接的,所以在链接期间无法检测到不匹配。当每个此类程序被链接时,所有与其他程序阶段交互的输入或输出都被视为活跃的。链接器将生成假定接口另一侧存在兼容程序的可执行代码。如果出现程序间的不匹配,不会生成 GL 错误,但接口上的某些或全部输入将变为未定义。

  • 在程序对象间的接口之间,一组输入和输出被认为是精确匹配的,当且仅当:

    • 每个声明的输入块或变量都有描述中的匹配输出;
    • 内建声明的 gl_PerVertex 着色器接口块必须被重新声明,并且重新声明的 gl_PerVertex 块的所有成员(包括 redeclaration 中可能存在的 gl_PointSize 成员)在名称、类型、限定符和声明顺序上必须完全匹配。
    • 不应有未声明匹配输入块或变量声明的输出块或用户自定义输出变量。
  • 当程序间接口的输入和输出精确匹配时,除了对应输出在前一着色器中未被写入的情况外,所有输入都是明确定义的。然而,任何输入和输出之间的不匹配都会导致所有输入成为未定义,除非特别指出的例外情况。即使某个输入有一个完全匹配的输出,其他输入或输出的不匹配也可能对生成读取或写入匹配变量的可执行代码产生不利影响。

  • 当使用输入和输出位置限定符时(OpenGL 着色语言规范第4.4.1节“Input Layout Qualifiers”和第4.4.2节“Output Layout Qualifiers”),程序间接口的输入和输出不必完全匹配。通过位置限定符匹配时,只要其他程序向匹配输出进行写入,任何具有输入位置限定符的输入都将被正确定义。在这种情况下,变量名称无需匹配。

  • 此外,带有位置布局限定符的标量和向量输入在满足以下条件时,将被明确定义:

    • 输入和输出在限定符(包括位置布局限定符)上完全匹配;
    • 输出是一个基础组件类型相同且组件数量大于输入的向量;
    • 输入和输出的公共组件类型为 int、uint 或 float(排除双精度组件类型的标量、向量和矩阵)。
      在这种情况下,输入的组件将从匹配输出的第一个组件获取,而输出的额外组件将被忽略。

SPIR-V着色器接口匹配也遵循本节所述规则,不论它们是否添加或覆盖7.4.1节给出的规则。最重要的是,SPIR-V变量和结构成员没有名称,因此不会通过名称字符串进行接口匹配。

构成着色器阶段输入或输出接口的所有变量必须作为OpEntryPoint指令的操作数,并在SPIR-V模块中分别声明为Input或Output存储类。

满足以下要求的着色器内建变量定义了内建接口块:

  • 明确声明(无隐式内建变量);
  • 使用BuiltIn装饰;
  • 在顶级成员为内建变量的块中声明;
  • 不具有Location或Component装饰。

只有当内建变量在这样的块中声明时,它们才会参与接口匹配。每个着色器每个接口最多只能有一个内建接口块。

用户定义的接口变量必须用Location装饰,并可以使用Component装饰。这些对应于7.4.1节讨论的位置和组件。Uniform和Shader存储块变量也必须使用Binding装饰。

用户定义的输出变量仅当两个变量使用相同的Location和Component装饰,并在类型和装饰上匹配(插值装饰除外)时,才被认为与后续阶段的输入变量匹配。

声明为结构体的变量或块成员仅在其结构体成员在类型、装饰、数量和声明顺序上完全匹配时,才被视为类型匹配。声明为数组的变量或块成员仅在两个声明都指定了相同元素类型和尺寸时,才被视为类型匹配。

在两个非片段着色器阶段之间的接口处,内建接口块必须严格如上所述完全匹配。在涉及片段着色器输入的接口中,任何内建输出的存在或缺失都不会影响接口匹配。

在两个着色器阶段之间的接口处,用户定义变量接口必须精确匹配。另外,如果存在满足以下所有条件的相应输出,标量和向量输入是有良好定义的:

  • 输入和输出在装饰上完全匹配;
  • 输出是一个基础类型相同且至少具有与输入一样多的组件的向量;
  • 输入和输出的公共组件类型为32位整型或浮点型(排除64位组件类型)。

在这种情况下,输入的组件将从输出的第一个组件中获取,输出的额外组件将被忽略。


程序二进制文件 Program Binaries

glGetProgramBinary 用于获取已编译和链接的着色器程序(program)的二进制表示形式。该二进制数据可以被存储并稍后通过 glProgramBinary 函数重新加载到程序对象中,从而提高程序加载效率,减少重复编译和链接的时间。

函数原型如下:

void glGetProgramBinary(GLuint program, GLsizei bufSize, GLsizei *length, GLenum *binaryFormat, void *binary);

参数解释:

  • GLuint program: 需要获取二进制表示形式的程序对象标识符。
  • GLsizei bufSize: 指定提供的缓冲区大小(以字节为单位),用于存放程序的二进制数据。
  • GLsizei *length: 输出参数,实际写入缓冲区的二进制数据长度将返回到这里。如果为NULL,则不会返回长度信息。
  • GLenum *binaryFormat: 输出参数,用于接收程序二进制格式的枚举值。
  • void *binary: 提供的缓冲区指针,用于存储程序的二进制数据。

在调用此函数之前,可以通过调用 glGetProgramiv 函数,并传入 GL_PROGRAM_BINARY_LENGTH 参数来查询程序二进制数据所需的最小缓冲区大小,确保提供足够大的缓冲区来接收完整数据。


void glProgramBinary( uint program, enum binaryFormat, const void *binary, sizei length );

这个命令用于将之前通过 glGetProgramBinary 获取的程序二进制数据加载到指定的程序对象中。这样做有助于避免在线编译,同时仍然可以使用OpenGL着色语言源代码作为可移植的初始格式。

  • program: 要加载二进制数据的目标程序对象ID。
  • binaryFormat: 必须与先前调用 glGetProgramBinary 返回的二进制格式相同。
  • binary: 指向包含程序二进制数据的缓冲区。
  • length: 程序二进制数据的长度,应与之前 glGetProgramBinary 或者通过 GetProgramiv 函数查询(参数为 GL_PROGRAM_BINARY_LENGTH)得到的结果一致。

如果这些条件不满足,加载程序二进制将失败,并将 programLINK_STATUS 设置为 FALSE

当硬件或软件配置发生变化,如使用的编译器版本不兼容或已过期时,加载程序二进制也可能失败。在这种情况下,应用应退回到提供原始的OpenGL着色语言源代码,并可能重新获取程序二进制以供将来使用。

成功调用 glProgramBinary 会用提供的二进制数据替换程序对象中的现有二进制数据,类似于隐式链接操作。LinkProgramglProgramBinary 都会通过 GetProgramiv 查询来设置程序对象的 LINK_STATUSTRUEFALSE,以反映链接成功或失败,并通过 GetProgramInfoLog 查询更新信息日志以提供有关警告或错误的详细信息。

成功调用 glProgramBinary 还会将默认统一块中的所有统一变量、所有统一块缓冲绑定以及所有着色器存储块缓冲绑定重置为其初始值。初始值是根据原始着色器源代码中指定的变量初始化器确定的,如果没有初始化器,则为零。

此外,以下状态在保存程序二进制前链接时生效的值,在成功调用 glProgramBinary 时会被恢复:

  • 程序参数 PROGRAM_SEPARABLE
  • 所有顶点着色器输入和片段着色器输出分配
  • 原子计数器绑定、偏移量和步幅分配

如果 glProgramBinary 加载二进制失败,不会生成错误,但关于该程序对象先前链接或加载的所有信息将会丢失。因此,加载失败不会恢复程序原有的状态。加载失败不会改变不受链接影响的其他程序状态,例如附加的着色器和通过 BindAttribLocationBindFragDataLocation 设置的顶点属性和片段数据位置绑定。

OpenGL没有定义特定的二进制格式。对 NUM_PROGRAM_BINARY_FORMATSPROGRAM_BINARY_FORMATS 的查询返回实现支持的程序二进制格式的数量和列表。由 glGetProgramBinary 返回的 binaryFormat 必须出现在这个列表中。

在同一配置下,通过 glGetProgramBinary 获取并使用 glProgramBinary 提交的任何程序二进制都必须能成功加载。通过 glProgramBinary 成功加载的所有程序必须能在任何合法的GL状态向量下正确运行。

如果实现需要基于程序之外的GL状态重新编译或修改程序执行文件,glGetProgramBinary 必须保存足够的信息以便进行此类重新编译。

为了表明可能会检索程序二进制,应通过调用 ProgramParameteri 并设置 pnamePROGRAM_BINARY_RETRIEVABLE_HINTvalueTRUE 来指示。此设置将在下次成功调用 LinkProgramglProgramBinary 后才生效。另外,应用程序可以延迟 glGetProgramBinary 调用,直到在所有可能遇到的非程序状态向量下使用了该程序。这种延迟可能允许实现将更多信息保存在程序二进制中,从而在未来使用程序二进制时最大限度地减少重新编译。

#include 
#include 
#include 

// 假设你已经有了一个已编译和链接的程序对象
GLuint program_id; // 替换为实际生成的程序对象ID

// 查询二进制格式的数量和大小
GLint num_formats;
GLint binary_length;

glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &num_formats);
glGetProgramiv(program_id, GL_PROGRAM_BINARY_LENGTH, &binary_length);

// 分配足够的内存来存储二进制数据
GLubyte* binary_data = new GLubyte[binary_length];

// 获取支持的二进制格式列表
GLenum* binary_formats = new GLenum[num_formats];
glGetIntegerv(GL_PROGRAM_BINARY_FORMATS, reinterpret_cast<GLint*>(binary_formats));

// 获取并存储程序的二进制表示
GLenum binary_format_used;
glGetProgramBinary(program_id, binary_length, NULL, &binary_format_used, binary_data);

// 将二进制数据保存到文件
std::ofstream binaryFile("program_binary.bin", std::ios::binary);
if (binaryFile.is_open()) {
    binaryFile.write(reinterpret_cast<char*>(binary_data), binary_length);
    binaryFile.close();
}

// 在未来某个时刻加载这个二进制数据
// ...

// 加载二进制数据到新的程序对象
GLuint new_program_id = glCreateProgram();

// 读取之前保存的二进制文件
std::ifstream binFile("program_binary.bin", std::ios::binary);
if (binFile.is_open()) {
    binFile.seekg(0, std::ios::end);
    binary_length = binFile.tellg();
    binFile.seekg(0, std::ios::beg);

    GLubyte* loaded_binary_data = new GLubyte[binary_length];
    binFile.read(reinterpret_cast<char*>(loaded_binary_data), binary_length);
    binFile.close();

    // 使用正确格式加载二进制数据
    glProgramBinary(new_program_id, binary_format_used, loaded_binary_data, binary_length);

    // 检查加载是否成功
    GLint link_status;
    glGetProgramiv(new_program_id, GL_LINK_STATUS, &link_status);

    if (link_status == GL_FALSE) {
        GLint info_log_length;
        glGetProgramiv(new_program_id, GL_INFO_LOG_LENGTH, &info_log_length);

        GLchar* info_log = new GLchar[info_log_length];
        glGetProgramInfoLog(new_program_id, info_log_length, NULL, info_log);

        std::cerr << "Failed to load program binary: " << info_log << std::endl;
        delete[] info_log;
    } else {
        // 成功加载,现在可以使用新的程序对象进行渲染了
    }

    delete[] loaded_binary_data;
}

delete[] binary_data;
delete[] binary_formats;

你可能感兴趣的:(OpenGL,图形渲染)