Opengl 使用Rust宏生成和编译着色器

Opengl 使用Rust宏生成和编译着色器,opengl,macros,rust,Opengl,Macros,Rust,这种方法起源于OpenGL着色器编程,但问题更抽象。我将编写一些伪代码来澄清我的意思 在OpenGL中,渲染是在所谓的“着色器”中完成的。着色器是应用于数据集的每个元素的计算内核,但其优点是计算是在GPU上执行的,因此利用GPU的并发性在同一时间进行尽可能多的计算 问题是着色器在编译时以文本形式显示,并且着色器需要在运行时由GPU的驱动程序编译。这意味着在每个程序开始时,init函数需要将每个着色器源文件编译成程序,然后才能调用着色器。下面是一个示例,请记住它是简化的伪代码: let shade

这种方法起源于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]
}
let shader_src_A=r#”
属性浮动a;
属性浮动b;
外浮b;
void main(){
b=a*b;
}
"#;
让着色器_src_B=r#”
属性浮动a;
属性浮动b;
外浮b;
void main(){
b=a+b;
}
"#;
让mut程序_A:ShaderProgram;
let mut program_B:ShaderProgram;
fn init(){
initGL();
program_A=编译_和链接(shader_src_A);
program_B=编译和链接(shader_src_B);
}
fn render(){
设data1=vec![1,2,3,4];
设data2=vec![5,6,7,8];
//将数据移动到gpu
让gpu_data_1=将_移动到_gpu(数据1);
让gpu_data_2=将_移动到_gpu(数据2);
让gpu_data_3:GpuData;
让gpu_data_4:GpuData;
方案A(
(gpu_数据_1,gpu_数据_2)//输入
(gpu_数据_3,)//输出
);
程序(
(gpu_数据_1,gpu_数据_2)//输入
(gpu_数据_4,)//输出
);
让数据_3=将_移动到_cpu(gpu_数据_3);
让数据_4=将_移动到_cpu(gpu_数据_4);
println!(“数据3{:?}数据4{:?}”,数据3,数据4);
//数据_3[5,12,21,32]数据_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);
}
fn init(){
initGL();
神秘宏!();
}
fn render(){
设data1=vec![1,2,3,4];
设data2=vec![5,6,7,8];
//将数据移动到gpu
让gpu_data_1=将_移动到_gpu(数据1);
让gpu_data_2=将_移动到_gpu(数据2);
让gpu_data_3:GpuData;
让gpu_data_4:GpuData;
树荫!(
(gpu_数据_1,gpu_数据_2),//输入元组
(gpu_data_3,),//输出元组
“gpu_data_3=gpu_data_1*gpu_data_2;”//这是着色器源,其余应由宏生成。
);
树荫!(
(gpu_数据_1,gpu_数据_2),//输入元组
(gpu_data_3,),//输出元组
“gpu_data_4=gpu_data_1+gpu_data_2;”//这是着色器源,其余应由宏生成。
);
让数据_3=将_移动到_cpu(gpu_数据_3);
让数据_4=将_移动到_cpu(gpu_数据_4);
println!(“数据3{:?}数据4{:?}”,数据3,数据4);
}
关键的区别在于,我没有一个通用的地方来编写所有着色器。我在调用着色器的地方编写着色器,并且不编写可以由其他参数推断的着色器部分。生成缺少的着色器部分应该是直截了当的,问题在于着色器的编译。每次调用时调用每个着色器编译的渲染器速度太慢,根本没有用处。其思想是,宏应该生成所有着色器源和程序的公共位置,以便
init
函数可以在程序启动时编译和链接所有程序

尽管有这个标题,我也同意用不同的方法解决我的问题,但我更喜欢所有程序都可以在
init
函数中编译的方法

编辑:

我还可以想象,shade不是一个宏,而是一个占位符no op函数,然后宏将对shade函数进行操作,通过遍历AST,它可以找到对shade的所有调用,并创建需要在init函数中执行的所有操作。

来自上一节(我的重点):

宏允许我们在语法级别进行抽象。宏调用是“扩展”语法形式的简写。这种扩展发生在编译的早期,在任何静态检查之前。因此,宏可以捕获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();
}
这里最大的问题是重复编译,因此我们可以使用板条箱延迟编译一次:

然后,您可以更进一步,围绕该宏生成另一个宏:

// 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编译时进行),但对于所需的结构来说根本没有意义。如果你可以编写简单的代码来完成你想要的,那么你就可以制作一个宏来让它更漂亮。我只是不相信有可能写出你想要的代码


有可能a可以做你想做的事情,但我还没有足够的经验来合理地判定它的存在与否。

所以你试图在给定着色器源的init函数中从完全不同的代码区域生成代码?我不确定那是不是阿宝
// 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();
}