1 个不稳定版本
0.1.0 | 2022年7月19日 |
---|
#34 in #edsl
在 shades 中使用
36KB
898 行
Shades,Rust 中的着色语言 EDSL
该包提供了一个 EDSL 来构建 着色器,利用 Rust 编译器(rustc
)和其类型系统来确保正确性和类型安全。因为着色器是用 Rust 编写的,所以这个包对语言无感知:从理论上讲,它可以针对任何着色语言——当前的一级语言是 GLSL。这个 EDSL 允许静态类型化着色器,同时在运行时生成实际的着色代码。
动机
在典型的图形库和引擎中,着色器是 不透明的字符串 ——要么在程序中硬编码,要么在运行时从文件中读取,要么通过字符串片段拼接而成等。这些字符串被传递给图形驱动程序,驱动程序将在运行时编译和链接代码。检查错误并正确反应是运行时的责任(即图形库、引擎或应用程序)。着色语言也可以离线编译,并且它们的字节码将在运行时使用(例如 SPIR-V)。
对于很多人来说,几十年来这已经足够好,甚至允许 实时编码:因为着色代码是在运行时加载的,所以每次更改发生时都可以重新加载、重新编译和重新链接它。然而,这也带来了不可忽视的缺点
- 着色代码通常在运行时进行检查。在这种情况下,编写不良的着色器直到运行时执行,GPU 驱动程序拒绝着色代码,程序员才看不到。
- 离线编译为字节码时,需要额外的专业工具(如外部程序、语言扩展等)。
- 编写着色器意味着学习一门新的语言。最广泛使用的着色语言是GLSL,但还有其他语言存在,这意味着人们需要学习专门的编程语言,并且在大多数情况下,编译系统较弱。例如,GLSL没有内置的功能来包含其他GLSL文件,它是一种老旧的类似C语言。
- 尽管使用动态语言的方式可能很有吸引力,但从动态语言转向静态使用并不是一件容易的事情。然而,反过来(从静态到动态)要简单得多。换句话说:借助低级系统原语,如
dlopen
、dlsym
等,可以实现实时重新加载编译语言。这需要更多的工作,但却是可能的。而且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类型,如
bool
、f32
或[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