使用 Rust 宏生成和编译着色器
Use a Rust macro to generate and compile a shader
我的这种方法起源于 OpenGL 着色器编程,但问题更抽象。我会写一些伪代码来阐明我的意思。
在 OpenGL 中,渲染是在所谓的 "shaders" 中完成的。着色器是应用于数据集的每个元素的计算内核,但其优点是计算在 GPU 上执行,因此利用 GPU 的并发特性尽可能同时计算.
问题在于着色器在编译时以文本形式呈现,而着色器需要在运行时由 GPU 的 driver 编译。这意味着在每个程序开始时,一个 init
函数需要在调用着色器之前将每个着色器源文件编译成程序。这是一个示例,请记住它是简化的伪代码:
let shader_src_A = r#"
attribute float a;
attribute float b;
out float b;
void main() {
b = a * b;
}
"#;
let shader_src_B = r#"
attribute float a;
attribute float b;
out float b;
void main() {
b = a + b;
}
"#;
let mut program_A : ShaderProgram;
let mut program_B : ShaderProgram;
fn init() {
initGL();
program_A = compile_and_link(shader_src_A);
program_B = compile_and_link(shader_src_B);
}
fn render() {
let data1 = vec![1,2,3,4];
let data2 = vec![5,6,7,8];
// move data to the gpu
let gpu_data_1 = move_to_gpu(data1);
let gpu_data_2 = move_to_gpu(data2);
let gpu_data_3 : GpuData<float>;
let gpu_data_4 : GpuData<float>;
program_A(
(gpu_data_1, gpu_data_2) // input
(gpu_data_3,) // output
);
program_B(
(gpu_data_1, gpu_data_2) // input
(gpu_data_4,) // output
);
let data_3 = move_to_cpu(gpu_data_3);
let data_4 = move_to_cpu(gpu_data_4);
println!("data_3 {:?} data_4 {:?}", data_3, data_4);
// data_3 [5, 12, 21, 32] data_4 [6, 8, 10, 12]
}
我的目标是能够写出这样的东西:
fn init() {
initGL();
mystery_macro!();
}
fn render() {
let data1 = vec![1,2,3,4];
let data2 = vec![5,6,7,8];
// move data to the gpu
let gpu_data_1 = move_to_gpu(data1);
let gpu_data_2 = move_to_gpu(data2);
let gpu_data_3 : GpuData<float>;
let gpu_data_4 : GpuData<float>;
shade!(
(gpu_data_1, gpu_data_2), // input tuple
(gpu_data_3,), // output tuple
"gpu_data_3 = gpu_data_1 * gpu_data_2;" // this is the shader source, the rest should be generated by the macro.
);
shade!(
(gpu_data_1, gpu_data_2), // input tuple
(gpu_data_3,), // output tuple
"gpu_data_4 = gpu_data_1 + gpu_data_2;" // this is the shader source, the rest should be generated by the macro.
);
let data_3 = move_to_cpu(gpu_data_3);
let data_4 = move_to_cpu(gpu_data_4);
println!("data_3 {:?} data_4 {:?}", data_3, data_4);
}
主要区别在于我没有一个共同的地方来编写我的所有着色器。我在调用它们的地方编写我的着色器,并且我不编写可以由其他参数推断的着色器部分。生成缺少的着色器部分应该很简单,问题是着色器的编译。在每次调用时调用每个着色器的编译的渲染器太慢了,根本没有用。这个想法是宏应该生成所有着色器源和程序的公共位置,以便 init
函数可以在程序启动时编译和 link 所有程序。
尽管标题不同,但我也可以接受以不同方式解决我的问题的解决方案,但我更喜欢可以在 init
函数中编译所有程序的解决方案。
编辑:
我也可以想象,那个shade不是一个宏,而是一个占位符no-op函数,然后这个宏会对shade函数进行操作,通过遍历AST,可以找到所有对shade的调用, 并在 init 函数中创建所有需要完成的事情。
来自 The Rust Programming Language section on macros(强调我的):
Macros allow us to abstract at a syntactic level. A macro invocation is shorthand for an "expanded" syntactic form. This expansion happens early in compilation, before any static checking. As a result, macros can capture many patterns of code reuse that Rust’s core abstractions cannot.
换句话说,只有当您已经拥有一些具有可观样板的代码时,宏才有用。他们不能做超出代码本身的事情。
此外,Rust 宏的工作级别高于 C 宏。 Rust 宏不与原始文本一起呈现,而是包含程序的一些 AST 片段。
让我们从这个简化版本开始:
struct Shader(usize);
impl Shader {
fn compile(source: &str) -> Shader {
println!("Compiling a shader");
Shader(source.len())
}
fn run(&self) {
println!("Running a shader {}", self.0)
}
}
fn main() {
for _ in 0..10 {
inner_loop();
}
}
fn inner_loop() {
let shader_1_src = r#"add 1 + 1"#;
let shader_1 = Shader::compile(shader_1_src);
let shader_2_src = r#"add 42 + 53"#;
let shader_2 = Shader::compile(shader_2_src);
shader_1.run();
shader_2.run();
}
这里最大的问题是重复编译,所以我们可以使用lazy_static crate懒惰编译一次:
#[macro_use]
extern crate lazy_static;
// Previous code...
fn inner_loop() {
const SHADER_1_SRC: &'static str = r#"add 1 + 1"#;
lazy_static! {
static ref SHADER_1: Shader = Shader::compile(SHADER_1_SRC);
}
const SHADER_2_SRC: &'static str = r#"add 42 + 53"#;
lazy_static! {
static ref SHADER_2: Shader = Shader::compile(SHADER_2_SRC);
}
SHADER_1.run();
SHADER_2.run();
}
然后您可以更进一步,围绕它制作另一个宏:
// Previous code...
macro_rules! shader {
($src_name: ident, $name: ident, $l: expr, $r: expr) => {
const $src_name: &'static str = concat!("add ", $l, " + ", $r);
lazy_static! {
static ref $name: Shader = Shader::compile($src_name);
}
}
}
fn inner_loop() {
shader!(S1, SHADER_1, "1", "2");
shader!(S2, SHADER_2, "42", "53");
SHADER_1.run();
SHADER_2.run();
}
请注意,我们必须为内部源常量提供一个名称,因为目前无法在宏中生成任意标识符。
我不是游戏程序员,但这种类型的代码会让我保持警惕。在任何时候,您都可能会执行一些着色器编译,从而减慢您的程序。我同意在程序启动时预编译你所有的着色器是最有意义的(或者在 Rust 编译时,如果可能的话!),但它对你想要的结构来说根本没有意义。如果您可以编写纯 Rust 代码来执行您想要的操作,那么您可以创建一个宏来使其更漂亮。我只是不相信可以编写出你想要的 Rust 代码。
syntax extension 有可能做你想做的事,但我还没有足够的经验来明确地判断它是否可行。
我的这种方法起源于 OpenGL 着色器编程,但问题更抽象。我会写一些伪代码来阐明我的意思。
在 OpenGL 中,渲染是在所谓的 "shaders" 中完成的。着色器是应用于数据集的每个元素的计算内核,但其优点是计算在 GPU 上执行,因此利用 GPU 的并发特性尽可能同时计算.
问题在于着色器在编译时以文本形式呈现,而着色器需要在运行时由 GPU 的 driver 编译。这意味着在每个程序开始时,一个 init
函数需要在调用着色器之前将每个着色器源文件编译成程序。这是一个示例,请记住它是简化的伪代码:
let shader_src_A = r#"
attribute float a;
attribute float b;
out float b;
void main() {
b = a * b;
}
"#;
let shader_src_B = r#"
attribute float a;
attribute float b;
out float b;
void main() {
b = a + b;
}
"#;
let mut program_A : ShaderProgram;
let mut program_B : ShaderProgram;
fn init() {
initGL();
program_A = compile_and_link(shader_src_A);
program_B = compile_and_link(shader_src_B);
}
fn render() {
let data1 = vec![1,2,3,4];
let data2 = vec![5,6,7,8];
// move data to the gpu
let gpu_data_1 = move_to_gpu(data1);
let gpu_data_2 = move_to_gpu(data2);
let gpu_data_3 : GpuData<float>;
let gpu_data_4 : GpuData<float>;
program_A(
(gpu_data_1, gpu_data_2) // input
(gpu_data_3,) // output
);
program_B(
(gpu_data_1, gpu_data_2) // input
(gpu_data_4,) // output
);
let data_3 = move_to_cpu(gpu_data_3);
let data_4 = move_to_cpu(gpu_data_4);
println!("data_3 {:?} data_4 {:?}", data_3, data_4);
// data_3 [5, 12, 21, 32] data_4 [6, 8, 10, 12]
}
我的目标是能够写出这样的东西:
fn init() {
initGL();
mystery_macro!();
}
fn render() {
let data1 = vec![1,2,3,4];
let data2 = vec![5,6,7,8];
// move data to the gpu
let gpu_data_1 = move_to_gpu(data1);
let gpu_data_2 = move_to_gpu(data2);
let gpu_data_3 : GpuData<float>;
let gpu_data_4 : GpuData<float>;
shade!(
(gpu_data_1, gpu_data_2), // input tuple
(gpu_data_3,), // output tuple
"gpu_data_3 = gpu_data_1 * gpu_data_2;" // this is the shader source, the rest should be generated by the macro.
);
shade!(
(gpu_data_1, gpu_data_2), // input tuple
(gpu_data_3,), // output tuple
"gpu_data_4 = gpu_data_1 + gpu_data_2;" // this is the shader source, the rest should be generated by the macro.
);
let data_3 = move_to_cpu(gpu_data_3);
let data_4 = move_to_cpu(gpu_data_4);
println!("data_3 {:?} data_4 {:?}", data_3, data_4);
}
主要区别在于我没有一个共同的地方来编写我的所有着色器。我在调用它们的地方编写我的着色器,并且我不编写可以由其他参数推断的着色器部分。生成缺少的着色器部分应该很简单,问题是着色器的编译。在每次调用时调用每个着色器的编译的渲染器太慢了,根本没有用。这个想法是宏应该生成所有着色器源和程序的公共位置,以便 init
函数可以在程序启动时编译和 link 所有程序。
尽管标题不同,但我也可以接受以不同方式解决我的问题的解决方案,但我更喜欢可以在 init
函数中编译所有程序的解决方案。
编辑:
我也可以想象,那个shade不是一个宏,而是一个占位符no-op函数,然后这个宏会对shade函数进行操作,通过遍历AST,可以找到所有对shade的调用, 并在 init 函数中创建所有需要完成的事情。
来自 The Rust Programming Language section on macros(强调我的):
Macros allow us to abstract at a syntactic level. A macro invocation is shorthand for an "expanded" syntactic form. This expansion happens early in compilation, before any static checking. As a result, macros can capture many patterns of code reuse that Rust’s core abstractions cannot.
换句话说,只有当您已经拥有一些具有可观样板的代码时,宏才有用。他们不能做超出代码本身的事情。
此外,Rust 宏的工作级别高于 C 宏。 Rust 宏不与原始文本一起呈现,而是包含程序的一些 AST 片段。
让我们从这个简化版本开始:
struct Shader(usize);
impl Shader {
fn compile(source: &str) -> Shader {
println!("Compiling a shader");
Shader(source.len())
}
fn run(&self) {
println!("Running a shader {}", self.0)
}
}
fn main() {
for _ in 0..10 {
inner_loop();
}
}
fn inner_loop() {
let shader_1_src = r#"add 1 + 1"#;
let shader_1 = Shader::compile(shader_1_src);
let shader_2_src = r#"add 42 + 53"#;
let shader_2 = Shader::compile(shader_2_src);
shader_1.run();
shader_2.run();
}
这里最大的问题是重复编译,所以我们可以使用lazy_static crate懒惰编译一次:
#[macro_use]
extern crate lazy_static;
// Previous code...
fn inner_loop() {
const SHADER_1_SRC: &'static str = r#"add 1 + 1"#;
lazy_static! {
static ref SHADER_1: Shader = Shader::compile(SHADER_1_SRC);
}
const SHADER_2_SRC: &'static str = r#"add 42 + 53"#;
lazy_static! {
static ref SHADER_2: Shader = Shader::compile(SHADER_2_SRC);
}
SHADER_1.run();
SHADER_2.run();
}
然后您可以更进一步,围绕它制作另一个宏:
// Previous code...
macro_rules! shader {
($src_name: ident, $name: ident, $l: expr, $r: expr) => {
const $src_name: &'static str = concat!("add ", $l, " + ", $r);
lazy_static! {
static ref $name: Shader = Shader::compile($src_name);
}
}
}
fn inner_loop() {
shader!(S1, SHADER_1, "1", "2");
shader!(S2, SHADER_2, "42", "53");
SHADER_1.run();
SHADER_2.run();
}
请注意,我们必须为内部源常量提供一个名称,因为目前无法在宏中生成任意标识符。
我不是游戏程序员,但这种类型的代码会让我保持警惕。在任何时候,您都可能会执行一些着色器编译,从而减慢您的程序。我同意在程序启动时预编译你所有的着色器是最有意义的(或者在 Rust 编译时,如果可能的话!),但它对你想要的结构来说根本没有意义。如果您可以编写纯 Rust 代码来执行您想要的操作,那么您可以创建一个宏来使其更漂亮。我只是不相信可以编写出你想要的 Rust 代码。
syntax extension 有可能做你想做的事,但我还没有足够的经验来明确地判断它是否可行。