使用Rust宏生成并编译着色器

时间:2015-12-05 00:30:10

标签: opengl macros rust

我的这种方法的起源来自OpenGL着色器编程,但问题更抽象。我会写一些伪代码来澄清我的意思。

在OpenGL中,渲染是在所谓的“着色器”中完成的。着色器是应用于数据集的每个元素的计算内核,但具有以下优点:计算在GPU上执行,因此利用GPU的并发特性来尽可能多地同时计算

问题是着色器在编译时呈现为文本,并且着色器需要在运行时由GPU的驱动程序编译。这意味着在每个程序开始时,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函数可以在程序启动时编译和链接所有程序。

尽管有标题,我也可以使用一种解决方案来解决我的问题,但我更喜欢一种解决方案,其中所有程序都可以在init函数中编译。

编辑:

我还可以想象,阴影不是一个宏,而是一个占位符无操作函数,然后宏将在阴影函数上运行,并且通过遍历AST,它可以找到所有调用阴影,并创建所有内容这需要在init函数中完成。

1 个答案:

答案 0 :(得分:2)

来自The Rust Programming Languagemacros部分(强调我的):

  

宏允许我们在句法层面进行抽象。宏调用是&#34;扩展&#34;的简写。句法形式。在任何静态检查之前,这种扩展在编译的早期发生。因此,宏可以捕获Rust的核心抽象所不具备的许多代码重用模式

换句话说,只有当已经拥有一些具有明显样板的代码时,宏才有用。他们不能做超出代码本身的事情。

此外,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包装箱懒洋洋地编译一次:

#[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有可能做到你想做的事情,但我没有足够的经验来对它们进行合理的判断。