Skip to main content

proof_engine/render/postfx/
pipeline.rs

1//! GPU post-processing pipeline: bloom, chromatic aberration, film grain, vignette, scanlines.
2//!
3//! Pipeline:
4//!   1. Glyphs render into scene FBO (2 color attachments: color + emission)
5//!   2. Emission texture → horizontal Gaussian blur → bloom_tex[0]
6//!   3. bloom_tex[0]    → vertical   Gaussian blur → bloom_tex[1]
7//!   4. (extra pass for softer bloom)
8//!   5. Composite: scene_color + bloom + chromatic aberration + grain + vignette → screen
9
10use glow::HasContext;
11use crate::render::shaders::{FULLSCREEN_VERT, BLOOM_FRAG, COMPOSITE_FRAG};
12use crate::config::RenderConfig;
13
14pub struct PostFxPipeline {
15    // Scene FBO — dual color attachments
16    pub scene_fbo:          glow::Framebuffer,
17    pub scene_color_tex:    glow::Texture,    // attachment 0: rendered scene
18    pub scene_emission_tex: glow::Texture,    // attachment 1: emission → bloom input
19
20    // Bloom ping-pong (half-res)
21    bloom_fbo: [glow::Framebuffer; 2],
22    bloom_tex: [glow::Texture; 2],
23
24    // Programs
25    bloom_prog:     glow::Program,
26    composite_prog: glow::Program,
27
28    // Empty VAO for fullscreen draws (GL 3.3 Core requires a VAO bound)
29    fullscreen_vao: glow::VertexArray,
30}
31
32impl PostFxPipeline {
33    pub unsafe fn new(gl: &glow::Context, width: u32, height: u32) -> Self {
34        let bloom_prog     = compile_postfx_program(gl, FULLSCREEN_VERT, BLOOM_FRAG);
35        let composite_prog = compile_postfx_program(gl, FULLSCREEN_VERT, COMPOSITE_FRAG);
36        let fullscreen_vao = gl.create_vertex_array().expect("postfx fullscreen_vao");
37
38        // Pre-bind sampler units (never changes)
39        gl.use_program(Some(bloom_prog));
40        set_u_i32(gl, bloom_prog, "u_texture", 0);
41
42        gl.use_program(Some(composite_prog));
43        set_u_i32(gl, composite_prog, "u_scene", 0);
44        set_u_i32(gl, composite_prog, "u_bloom", 1);
45
46        let (scene_fbo, scene_color_tex, scene_emission_tex) =
47            create_scene_fbo(gl, width, height);
48        let (bloom_fbo, bloom_tex) =
49            create_bloom_fbos(gl, (width / 2).max(1), (height / 2).max(1));
50
51        Self {
52            scene_fbo,
53            scene_color_tex,
54            scene_emission_tex,
55            bloom_fbo,
56            bloom_tex,
57            bloom_prog,
58            composite_prog,
59            fullscreen_vao,
60        }
61    }
62
63    /// Recreate FBO textures after a window resize. Call from Pipeline::poll_events resize handler.
64    pub unsafe fn resize(&mut self, gl: &glow::Context, width: u32, height: u32) {
65        // Delete old resources
66        gl.delete_framebuffer(self.scene_fbo);
67        gl.delete_texture(self.scene_color_tex);
68        gl.delete_texture(self.scene_emission_tex);
69        for i in 0..2 {
70            gl.delete_framebuffer(self.bloom_fbo[i]);
71            gl.delete_texture(self.bloom_tex[i]);
72        }
73
74        let (scene_fbo, scene_color_tex, scene_emission_tex) =
75            create_scene_fbo(gl, width, height);
76        let (bloom_fbo, bloom_tex) =
77            create_bloom_fbos(gl, (width / 2).max(1), (height / 2).max(1));
78
79        self.scene_fbo          = scene_fbo;
80        self.scene_color_tex    = scene_color_tex;
81        self.scene_emission_tex = scene_emission_tex;
82        self.bloom_fbo          = bloom_fbo;
83        self.bloom_tex          = bloom_tex;
84    }
85
86    /// Run bloom passes then composite to the default (screen) framebuffer.
87    ///
88    /// Must be called after all glyphs have been drawn to scene_fbo.
89    pub unsafe fn run(
90        &self,
91        gl:     &glow::Context,
92        config: &RenderConfig,
93        full_w: u32,
94        full_h: u32,
95        time:   f32,
96    ) {
97        let bw = (full_w / 2).max(1) as i32;
98        let bh = (full_h / 2).max(1) as i32;
99
100        gl.bind_vertex_array(Some(self.fullscreen_vao));
101
102        // ── Bloom passes ─────────────────────────────────────────────────────
103        if config.bloom_enabled {
104            gl.use_program(Some(self.bloom_prog));
105            gl.active_texture(glow::TEXTURE0);
106
107            // Pass A: horizontal blur — emission_tex → bloom_tex[0]
108            gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.bloom_fbo[0]));
109            gl.viewport(0, 0, bw, bh);
110            gl.clear(glow::COLOR_BUFFER_BIT);
111            gl.bind_texture(glow::TEXTURE_2D, Some(self.scene_emission_tex));
112            set_u_bool(gl, self.bloom_prog, "u_horizontal", true);
113            set_u_f32(gl,  self.bloom_prog, "u_radius", 1.5);
114            gl.draw_arrays(glow::TRIANGLES, 0, 3);
115
116            // Pass B: vertical blur — bloom_tex[0] → bloom_tex[1]
117            gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.bloom_fbo[1]));
118            gl.clear(glow::COLOR_BUFFER_BIT);
119            gl.bind_texture(glow::TEXTURE_2D, Some(self.bloom_tex[0]));
120            set_u_bool(gl, self.bloom_prog, "u_horizontal", false);
121            gl.draw_arrays(glow::TRIANGLES, 0, 3);
122
123            // Pass C: second horizontal (wider) — bloom_tex[1] → bloom_tex[0]
124            gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.bloom_fbo[0]));
125            gl.clear(glow::COLOR_BUFFER_BIT);
126            gl.bind_texture(glow::TEXTURE_2D, Some(self.bloom_tex[1]));
127            set_u_bool(gl, self.bloom_prog, "u_horizontal", true);
128            set_u_f32(gl,  self.bloom_prog, "u_radius", 2.5);
129            gl.draw_arrays(glow::TRIANGLES, 0, 3);
130
131            // Pass D: second vertical — bloom_tex[0] → bloom_tex[1]
132            gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.bloom_fbo[1]));
133            gl.clear(glow::COLOR_BUFFER_BIT);
134            gl.bind_texture(glow::TEXTURE_2D, Some(self.bloom_tex[0]));
135            set_u_bool(gl, self.bloom_prog, "u_horizontal", false);
136            gl.draw_arrays(glow::TRIANGLES, 0, 3);
137        }
138
139        // ── Composite to screen ───────────────────────────────────────────────
140        gl.bind_framebuffer(glow::FRAMEBUFFER, None);
141        gl.viewport(0, 0, full_w as i32, full_h as i32);
142        gl.clear(glow::COLOR_BUFFER_BIT);
143        gl.use_program(Some(self.composite_prog));
144
145        // Texture unit 0: scene color
146        gl.active_texture(glow::TEXTURE0);
147        gl.bind_texture(glow::TEXTURE_2D, Some(self.scene_color_tex));
148
149        // Texture unit 1: bloom result (or scene color at zero intensity if disabled)
150        gl.active_texture(glow::TEXTURE1);
151        if config.bloom_enabled {
152            gl.bind_texture(glow::TEXTURE_2D, Some(self.bloom_tex[1]));
153        } else {
154            gl.bind_texture(glow::TEXTURE_2D, Some(self.scene_color_tex));
155        }
156
157        let bloom_intensity = if config.bloom_enabled { config.bloom_intensity } else { 0.0 };
158        set_u_f32(gl,  self.composite_prog, "u_bloom_intensity",    bloom_intensity);
159        set_u_vec3(gl, self.composite_prog, "u_tint",               [1.0, 1.0, 1.0]);
160        set_u_f32(gl,  self.composite_prog, "u_saturation",         1.0);
161        set_u_f32(gl,  self.composite_prog, "u_contrast",           1.05);
162        set_u_f32(gl,  self.composite_prog, "u_brightness",         0.0);
163        set_u_f32(gl,  self.composite_prog, "u_vignette",           0.25);
164        set_u_f32(gl,  self.composite_prog, "u_grain_intensity",    config.film_grain);
165        set_u_f32(gl,  self.composite_prog, "u_grain_seed",         time);
166        set_u_f32(gl,  self.composite_prog, "u_chromatic",          config.chromatic_aberration);
167        set_u_f32(gl,  self.composite_prog, "u_scanline_intensity",
168            if config.scanlines_enabled { 0.15 } else { 0.0 });
169        set_u_bool(gl, self.composite_prog, "u_scanlines_enabled",  config.scanlines_enabled);
170
171        gl.draw_arrays(glow::TRIANGLES, 0, 3);
172
173        gl.bind_vertex_array(None);
174    }
175}
176
177// ── FBO helpers ────────────────────────────────────────────────────────────────
178
179unsafe fn make_rgba_tex(gl: &glow::Context, w: u32, h: u32) -> glow::Texture {
180    let tex = gl.create_texture().expect("postfx texture");
181    gl.bind_texture(glow::TEXTURE_2D, Some(tex));
182    gl.tex_image_2d(
183        glow::TEXTURE_2D, 0, glow::RGBA as i32,
184        w as i32, h as i32, 0,
185        glow::RGBA, glow::UNSIGNED_BYTE, None,
186    );
187    gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
188    gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
189    gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S,     glow::CLAMP_TO_EDGE as i32);
190    gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T,     glow::CLAMP_TO_EDGE as i32);
191    tex
192}
193
194unsafe fn create_scene_fbo(
195    gl: &glow::Context, w: u32, h: u32,
196) -> (glow::Framebuffer, glow::Texture, glow::Texture) {
197    let color_tex    = make_rgba_tex(gl, w, h);
198    let emission_tex = make_rgba_tex(gl, w, h);
199
200    let fbo = gl.create_framebuffer().expect("scene fbo");
201    gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
202    gl.framebuffer_texture_2d(
203        glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some(color_tex), 0,
204    );
205    gl.framebuffer_texture_2d(
206        glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT1, glow::TEXTURE_2D, Some(emission_tex), 0,
207    );
208    gl.draw_buffers(&[glow::COLOR_ATTACHMENT0, glow::COLOR_ATTACHMENT1]);
209
210    let status = gl.check_framebuffer_status(glow::FRAMEBUFFER);
211    if status != glow::FRAMEBUFFER_COMPLETE {
212        log::error!("Scene FBO incomplete: 0x{status:X}");
213    }
214    gl.bind_framebuffer(glow::FRAMEBUFFER, None);
215    (fbo, color_tex, emission_tex)
216}
217
218unsafe fn create_bloom_fbos(
219    gl: &glow::Context, w: u32, h: u32,
220) -> ([glow::Framebuffer; 2], [glow::Texture; 2]) {
221    let textures = [make_rgba_tex(gl, w, h), make_rgba_tex(gl, w, h)];
222    let fbos = [
223        gl.create_framebuffer().expect("bloom fbo 0"),
224        gl.create_framebuffer().expect("bloom fbo 1"),
225    ];
226    for i in 0..2 {
227        gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbos[i]));
228        gl.framebuffer_texture_2d(
229            glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some(textures[i]), 0,
230        );
231        gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);
232    }
233    gl.bind_framebuffer(glow::FRAMEBUFFER, None);
234    (fbos, textures)
235}
236
237// ── Shader compilation ─────────────────────────────────────────────────────────
238
239unsafe fn compile_postfx_program(
240    gl: &glow::Context, vert_src: &str, frag_src: &str,
241) -> glow::Program {
242    let vs = gl.create_shader(glow::VERTEX_SHADER).expect("postfx vert shader");
243    gl.shader_source(vs, vert_src);
244    gl.compile_shader(vs);
245    if !gl.get_shader_compile_status(vs) {
246        panic!("PostFx vert compile error:\n{}", gl.get_shader_info_log(vs));
247    }
248
249    let fs = gl.create_shader(glow::FRAGMENT_SHADER).expect("postfx frag shader");
250    gl.shader_source(fs, frag_src);
251    gl.compile_shader(fs);
252    if !gl.get_shader_compile_status(fs) {
253        panic!("PostFx frag compile error:\n{}", gl.get_shader_info_log(fs));
254    }
255
256    let prog = gl.create_program().expect("postfx program");
257    gl.attach_shader(prog, vs);
258    gl.attach_shader(prog, fs);
259    gl.link_program(prog);
260    if !gl.get_program_link_status(prog) {
261        panic!("PostFx link error:\n{}", gl.get_program_info_log(prog));
262    }
263    gl.detach_shader(prog, vs);
264    gl.detach_shader(prog, fs);
265    gl.delete_shader(vs);
266    gl.delete_shader(fs);
267    prog
268}
269
270// ── Uniform helpers ────────────────────────────────────────────────────────────
271
272unsafe fn set_u_i32(gl: &glow::Context, prog: glow::Program, name: &str, v: i32) {
273    if let Some(loc) = gl.get_uniform_location(prog, name) {
274        gl.uniform_1_i32(Some(&loc), v);
275    }
276}
277
278unsafe fn set_u_bool(gl: &glow::Context, prog: glow::Program, name: &str, v: bool) {
279    if let Some(loc) = gl.get_uniform_location(prog, name) {
280        gl.uniform_1_i32(Some(&loc), v as i32);
281    }
282}
283
284unsafe fn set_u_f32(gl: &glow::Context, prog: glow::Program, name: &str, v: f32) {
285    if let Some(loc) = gl.get_uniform_location(prog, name) {
286        gl.uniform_1_f32(Some(&loc), v);
287    }
288}
289
290unsafe fn set_u_vec3(gl: &glow::Context, prog: glow::Program, name: &str, v: [f32; 3]) {
291    if let Some(loc) = gl.get_uniform_location(prog, name) {
292        gl.uniform_3_f32(Some(&loc), v[0], v[1], v[2]);
293    }
294}