#glsl #edsl #shader #spir-v #graphics

shades-edsl

着色语言领域的 EDSL

1 个不稳定版本

0.1.0 2022年7月19日

#34 in #edsl


shades 中使用

BSD-3-Clause 许可协议

36KB
898

Shades,Rust 中的着色语言 EDSL

该包提供了一个 EDSL 来构建 着色器,利用 Rust 编译器(rustc)和其类型系统来确保正确性和类型安全。因为着色器是用 Rust 编写的,所以这个包对语言无感知:从理论上讲,它可以针对任何着色语言——当前的一级语言是 GLSL。这个 EDSL 允许静态类型化着色器,同时在运行时生成实际的着色代码。

动机

在典型的图形库和引擎中,着色器是 不透明的字符串 ——要么在程序中硬编码,要么在运行时从文件中读取,要么通过字符串片段拼接而成等。这些字符串被传递给图形驱动程序,驱动程序将在运行时编译和链接代码。检查错误并正确反应是运行时的责任(即图形库、引擎或应用程序)。着色语言也可以离线编译,并且它们的字节码将在运行时使用(例如 SPIR-V)。

对于很多人来说,几十年来这已经足够好,甚至允许 实时编码:因为着色代码是在运行时加载的,所以每次更改发生时都可以重新加载、重新编译和重新链接它。然而,这也带来了不可忽视的缺点

  • 着色代码通常在运行时进行检查。在这种情况下,编写不良的着色器直到运行时执行,GPU 驱动程序拒绝着色代码,程序员才看不到。
  • 离线编译为字节码时,需要额外的专业工具(如外部程序、语言扩展等)。
  • 编写着色器意味着学习一门新的语言。最广泛使用的着色语言是GLSL,但还有其他语言存在,这意味着人们需要学习专门的编程语言,并且在大多数情况下,编译系统较弱。例如,GLSL没有内置的功能来包含其他GLSL文件,它是一种老旧的类似C语言。
  • 尽管使用动态语言的方式可能很有吸引力,但从动态语言转向静态使用并不是一件容易的事情。然而,反过来(从静态到动态)要简单得多。换句话说:借助低级系统原语,如dlopendlsym等,可以实现实时重新加载编译语言。这需要更多的工作,但却是可能的。而且Rust也能做到这一点

这个crate的作者(@phaazon)认为,着色器代码仍然是代码,应该像代码一样对待。很容易看出实时编码/重新加载的优势,但更重要的是提供一个经过静态证明正确且bug较少的着色器代码。此外,如上所述,使用编译方法并不会阻止编写可重定位的代码对象,编译并重新加载这个对象,这大致与实时编码具有相同的功能。

重要提示:这个crate通过rustc尽力在编译时捕获语义错误。然而,它可能仍然缺少捕捉所有语义错误的机会。如果您发现这种情况,请随时提交问题,因为这被认为是错误/回归。

另一个重要的问题是选择使用EDSL。有些人可能会争论说,Rust有其他有趣且强大的方法来实现相同的目标。重要的是要注意,这个crate不提供将Rust代码编译成着色语言的编译器。相反,它提供了一个在运行时生成着色器代码的Rust crate。其他替代方案可能是使用proc-macro。有几个crate就是这样做的

  • 您可以使用glsl和[glsl-quasiquote] crates。第一个是一个GLSL解析器,第二个允许您在quasi-quoter(glsl! { /* here */ })中编写GLSL,并在运行时编译和检查。尽管如此,它仍然是GLSL,而且运行时组合的可能性比EDSL要少得多。
  • 您可以使用rust-gpu项目。这是一个类似的项目,但他们使用proc-macro,将表示GPU代码的Rust代码编译成。它需要一个特定的工具链,并且不在这个crate的工作级别上——它甚至可以编译core库的大部分代码。

影响因素

  • blaze-html,一个用于在Haskell中构建HTML的Haskell EDSL
  • selda,一个Haskell嵌入式领域特定语言(EDSL),用于构建SQL查询并执行,无需编写SQL字符串。这个当前的crate在方法上非常相似。

为什么你会喜欢这个

如果你喜欢类型系统、语言以及基本上编译器黑客(为你的编译器编写代码以生成运行时代码!),那么你可能会喜欢这个crate。在所有功能中,你会发现

  • 使用纯Rust。因为这个crate是语言无关的,你开始所需了解的全部就是编写Rust。你不需要学习GLSL来使用这个crate——尽管你仍然需要理解着色器、它们是什么、它们是如何工作的等概念。但那些概念的实际编码现在由一个本地的Rust crate封装。
  • 用于表示着色器类型的类型是基本的原生Rust类型,如boolf32[T; N]
  • 编写更函数式而不是命令式的代码。例如,这个crate中的着色器基本是一个函数,它接受一个类型为Vertex的对象,并返回另一个对象,该对象将被传递到下一阶段。
  • rustc中捕获语义错误。例如,在着色器代码中将一个bool赋值给一个f32将触发一个rustc错误。
  • 使某些代码变得无法编写。例如,你将无法在顶点着色器中使用仅适用于片段着色器的语句,因为这本身是不可能的。
  • 扩展和向著名着色语言添加更多项目。例如,GLSL没有π常量。这种情况已经得到解决,所以你再也不必自己编写π的小数了。
  • 因为你编写Rust,所以你可以享受所有语言类型糖果、组合性、可扩展性和正确性。
  • do-notation crate的背后有一个实验性的monadic体验。这允许你使用do-notation crate编写着色器,为你移除大量样板代码,使作用域着色器作用域对你来说都是隐藏的,让你感觉像是在编写魔法着色器代码。

为什么你不会喜欢这个

截至今天,这个crate仍然非常实验性。以下是你可能不喜欢这个crate的一些事情

  • 当前的冗长度是不可接受的。大多数你需要使用的lambda都需要你为它们的参数进行注释,即使这些参数显然是可以猜测的。这种情况应该尽快解决,但人们必须知道,当前的情况意味着大量的类型注释。
  • 有些人会认为编写GLSL要快得多,也简单得多,他们是对的。然而,你首先需要学习GLSL;你将无法针对SPIR-V;你将没有静态类型问题的解决方案;等等。
  • 在着色器代码的运行时编译/链接失败的情况下,调试它可能具有挑战性,因为所有标识符(除少数例外)都是为你生成的。这将使理解生成的代码更困难。
  • 某些概念,尤其是控制流语句,看起来有点奇怪。例如,这个crate中用更复杂的方式编写了GLSL中的for循环。生成的代码相同,但通过这个crate它更冗长。

依赖项

~1.5MB
~35K SLoC