在图形应用程序中,为什么着色器会在运行时加载到应用程序中?

6

通常,使用GLSL等语言编写的着色器会在运行时加载到图形应用程序中。我想知道为什么不直接将着色器与应用程序一起编译,这样它们就不必后来再被加载了。像这样:

#define glsl(version, glsl) "#version " #version "\n" #glsl

namespace glsl { namespace vs {
//VERTEX SHADERS
//=========================
// simple VS
//=========================
constexpr GLchar * const simple = glsl(450 core, 
    layout(location = 0) in vec3 position;

    void main() {
        gl_Position = vec4(position, 1.0f);
    }
    );

} namespace fs {
//FRAGMENT SHADERS
//=========================
// simple FS
//=========================
constexpr GLchar * const simple = glsl(450 core,
    out vec4 color;

    void main() {
        color = vec4(1.0f, 0.0f, 0.0f, 1.0f);
    }       
    );

} }

我认为这不会导致太大的exe文件,并且可以加快加载时间;除非我对典型图形应用程序使用的着色器数量有误。我意识到您可能希望在编译后更新着色器,但这真的会发生吗?

有没有任何理由让我不想这样做?


5
这个问题可能更适合发布在https://softwareengineering.stackexchange.com/,因为它很可能会引发大量观点或主观的答案。 - Xirema
1
我不愿意这样做的主要原因是每当着色器改变时,您必须重新编译应用程序。在我的当前项目中,构建应用程序可能需要几分钟,因此我们尽可能避免这种情况。 - BDL
你从哪里得到这个想法的?着色器编译是一个昂贵的过程(在同一台机器上反复进行并获得相同结果是相当无意义的)。使用预编译(或至少缓存)的着色器是一种推荐的做法。例如,可以查看QUALCOMM开发者网络文章 - user7860670
1
@VTT:这绝对是一件好事。但是你仍然需要在每台新机器上编译你的着色器(有时候当驱动程序更新时也需要),所以总体问题仍然存在。此外,在开发过程中,每次启动时编译着色器可能是必要的。 - BDL
3个回答

4
几个原因:
  • 开发期间,将着色器作为独立文件存储非常方便进行“热加载”,以便在调试或性能调优时进行更改并立即查看结果。这在着色器作为单独文件存储时更加简单。
  • 如评论所述,根据您的平台,将着色器从高级着色语言预编译为中间字节码表示形式或实际的最终GPU代码(例如,在GPU硬件是固定目标的控制台情况下)很常见。着色器编译可能非常耗时,因此最好尽可能在离线状态下进行而不是在运行时进行。
  • 您采取的方法实际上不只是针对着色器的问题。在任何项目中,您都可以选择何时将资源嵌入可执行文件中(直接嵌入源代码或通过类似于Windows Resources的单独构建步骤),或将其存储为单独的文件。两种方法都有其优缺点,但嵌入的主要优势在于所有应用程序可能需要的资源都嵌入到可执行文件中,因此您不必担心或处理潜在的缺失资源。缺点是如果将所有内容都压入可执行文件中(特别是对于像游戏这样具有许多大型资产的项目),则会增加构建时间,使热加载变得困难,并可能使资产组织问题更加复杂。
  • 您采取的方法在小型/简单应用程序中实际上并不罕见,但随着您的应用程序变得越来越大且需要管理的着色器数量也越来越多,这种方法会变得更加麻烦。个人而言,即使在小型个人项目中,我也喜欢能够“热加载”着色器。

1

你怎么想它会加速加载时间?其实并不会!

无论你是从用fopen(或std::ifstreamCreateFile等)打开的普通文件中读取数据并从中读取,还是访问进程映像的某个部分,都没有关系。事实上,在大多数情况下,访问进程映像中的一些较大的页面块可能会表现得更差,因为大多数操作系统在保存非可执行、只读数据的段中懒惰地填充页面;对未填充页面的mmaped读取访问实际上比纯读取更慢,因为它涉及到故障页面表和后续的I/O读取以填充页面数据。

强制阅读:http://lkml.iu.edu/hypermail/linux/kernel/0004.0/0728.html

OpenGL API规定GLSL源代码是加载着色器的唯一方式。有一个缓存API(glShaderBinary,glProgramBinary),但通过它加载的二进制文件格式未指定,可能随时更改,并且驱动程序拒绝二进制文件是有效的错误条件,以强制加载原始的GLSL源代码。要实际获取这样的着色器二进制文件,您首先需要加载常规的GLSL着色器源代码,编译它,然后从OpenGL检索二进制blob。

我之所以这么说是因为进程已经在内存中,而从文件读取则需要从磁盘中读取。而使用更少的语句来完成某件事情几乎总会导致更快的代码。 - Keith Becker
@KeithBecker:它不是按照你想象的那样工作。在分页虚拟内存环境中,操作系统不会将整个二进制文件加载到RAM中!这将是极其浪费的,也完全没有必要。发生的情况是创建了一个页面映射,它的工作方式与(实际上使用完全相同的代码路径)磁盘交换内存相同:进程文件基本上被视为只读交换文件,并且仅将实际使用的部分拉入RAM中。即使您有一个GiB大的可执行文件,如果代码段只有几kiB,它将像微小的可执行文件一样快速启动。 - datenwolf
@KeithBecker:任何不属于代码段的内容在启动时实际上都不会被加载到RAM中。操作系统会创建一个页面映射,当(如果)你最终访问它时,这些部分才会被拉入内存。然而,这比直接读取到缓冲区要复杂。也就是说,相关页面必须被故障,即内存管理器必须为这些页面分配一些系统的物理内存(这不是一个便宜的操作,事实上相当缓慢)。 - datenwolf

0

着色器编译过程是OpenGL的GLSL编译器的一部分,虽然您可以将着色器硬编码到内存中作为字符串,但每次对着色器进行任何编辑时都需要重新编译。

在游戏编程中,通常不希望在对代码、着色器、库等的数百个部分进行小修改时重新编译整个项目。这就是为什么所有大型游戏引擎都有脚本引擎的主要原因之一,往往更好地编写额外的样板代码来将脚本集成到您的引擎中,而不是编译程序10次才能使您的对象的颜色/光泽度/位置完美无缺。试着将所有着色器硬编码到内存中以便在运行时编译,您会发现这变得多么繁琐!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接