Skip to main content

par_term_render/renderer/shaders/
background.rs

1//! Background (custom) shader initialisation and runtime management.
2//!
3//! Covers the `CustomShaderRenderer` lifecycle: creation at startup via
4//! [`init_custom_shader`] and runtime enable/disable/reload operations
5//! exposed as `impl Renderer` methods.
6
7use super::super::Renderer;
8use super::{CustomShaderEnableParams, CustomShaderInitParams};
9use crate::cell_renderer::CellRenderer;
10use crate::custom_shader_renderer::CustomShaderRenderer;
11
12/// Initialize the custom shader renderer if configured.
13///
14/// Returns `(renderer, shader_path, load_error)`. The error is reported rather than
15/// only logged so that a shader broken at startup surfaces in the Settings window
16/// the same way one broken by a hot reload already does.
17pub(super) fn init_custom_shader(
18    cell_renderer: &CellRenderer,
19    params: CustomShaderInitParams<'_>,
20) -> (Option<CustomShaderRenderer>, Option<String>, Option<String>) {
21    let CustomShaderInitParams {
22        size_width,
23        size_height,
24        window_padding,
25        path: custom_shader_path,
26        enabled: custom_shader_enabled,
27        animation: custom_shader_animation,
28        animation_speed: custom_shader_animation_speed,
29        window_opacity,
30        full_content: custom_shader_full_content,
31        brightness: custom_shader_brightness,
32        channel_paths: custom_shader_channel_paths,
33        cubemap_path: custom_shader_cubemap_path,
34        custom_uniforms,
35        use_background_as_channel0,
36        background_channel0_blend_mode,
37        auto_dim_under_text,
38        auto_dim_strength,
39    } = params;
40    log::info!(
41        "[shader-init] init_custom_shader: enabled={}, path={:?}",
42        custom_shader_enabled,
43        custom_shader_path
44    );
45    if !custom_shader_enabled {
46        log::info!("[shader-init] Skipping: custom_shader_enabled=false");
47        return (None, None, None);
48    }
49
50    let Some(shader_path) = custom_shader_path else {
51        log::info!("[shader-init] Skipping: custom_shader_path is None");
52        return (None, None, None);
53    };
54
55    let path = par_term_config::Config::shader_path(shader_path);
56    match CustomShaderRenderer::new(
57        cell_renderer.device(),
58        cell_renderer.queue(),
59        crate::custom_shader_renderer::CustomShaderRendererConfig {
60            surface_format: cell_renderer.surface_format(),
61            shader_path: &path,
62            width: size_width,
63            height: size_height,
64            animation_enabled: custom_shader_animation,
65            animation_speed: custom_shader_animation_speed,
66            window_opacity,
67            full_content_mode: custom_shader_full_content,
68            channel_paths: custom_shader_channel_paths,
69            cubemap_path: custom_shader_cubemap_path,
70            custom_uniforms,
71            background_channel0_blend_mode,
72        },
73    ) {
74        Ok(mut renderer) => {
75            renderer.update_cell_dimensions(
76                cell_renderer.cell_width(),
77                cell_renderer.cell_height(),
78                window_padding,
79            );
80            renderer.set_scale_factor(cell_renderer.scale_factor);
81            renderer.set_brightness(custom_shader_brightness);
82            renderer.set_auto_dim_under_text(auto_dim_under_text, auto_dim_strength);
83
84            // Apply use_background_as_channel0 setting
85            if use_background_as_channel0 {
86                // Sync background texture and set flag
87                let bg_texture = cell_renderer.get_background_as_channel_texture();
88                renderer.set_background_texture(cell_renderer.device(), bg_texture);
89                renderer.update_use_background_as_channel0(
90                    cell_renderer.device(),
91                    use_background_as_channel0,
92                );
93            }
94
95            log::info!(
96                "[SHADER] Custom shader renderer initialized from: {} (use_bg_as_ch0={})",
97                path.display(),
98                use_background_as_channel0
99            );
100            (Some(renderer), Some(shader_path.to_string()), None)
101        }
102        Err(e) => {
103            let error = format!("{:#}", e);
104            log::error!(
105                "[SHADER] ERROR: Failed to load custom shader '{}': {}",
106                path.display(),
107                error
108            );
109            (None, None, Some(error))
110        }
111    }
112}
113
114// ============================================================================
115// Background shader impl Renderer methods
116// ============================================================================
117
118impl Renderer {
119    /// Take the `(background, cursor)` shader failures recorded while this renderer
120    /// was constructed.
121    ///
122    /// Startup and hot-reload failures are the same failure, so the caller feeds
123    /// these into the same error sink the reload path uses; otherwise a shader
124    /// broken before launch degrades to an unshaded terminal with no diagnostic.
125    /// Consuming, so a rebuilt renderer does not re-report a stale error.
126    pub fn take_startup_shader_errors(&mut self) -> (Option<String>, Option<String>) {
127        (
128            self.startup_shader_error.take(),
129            self.startup_cursor_shader_error.take(),
130        )
131    }
132
133    /// Enable or disable animation for the custom shader at runtime
134    pub fn set_custom_shader_animation(&mut self, enabled: bool) {
135        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
136            custom_shader.set_animation_enabled(enabled);
137            self.dirty = true;
138        }
139    }
140
141    /// Update custom shader uniform values keyed by control name.
142    pub fn set_custom_shader_uniform_values(
143        &mut self,
144        values: std::collections::BTreeMap<String, par_term_config::ShaderUniformValue>,
145    ) {
146        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
147            custom_shader.set_custom_uniform_values(values);
148            self.dirty = true;
149        }
150    }
151
152    /// Reload the custom shader from source code.
153    ///
154    /// Compiles the new shader source and replaces the current pipeline.
155    /// If compilation fails, returns an error and the old shader remains active.
156    pub fn reload_shader_from_source(
157        &mut self,
158        source: &str,
159    ) -> Result<(), crate::error::RenderError> {
160        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
161            custom_shader
162                .reload_from_source(self.cell_renderer.device(), source, "editor")
163                .map_err(|e| crate::error::RenderError::NoActiveShader(format!("{:#}", e)))?;
164            self.dirty = true;
165            Ok(())
166        } else {
167            Err(crate::error::RenderError::NoActiveShader(
168                "No custom shader is currently loaded. Enable a custom shader first.".to_string(),
169            ))
170        }
171    }
172
173    /// Enable/disable custom shader at runtime.
174    ///
175    /// When enabling, tries to (re)load the shader from the given path; when disabling,
176    /// drops the renderer instance.
177    pub fn set_custom_shader_enabled(
178        &mut self,
179        params: CustomShaderEnableParams<'_>,
180    ) -> Result<(), String> {
181        let CustomShaderEnableParams {
182            enabled,
183            shader_path,
184            window_opacity,
185            animation_enabled,
186            animation_speed,
187            full_content,
188            brightness,
189            channel_paths,
190            cubemap_path,
191            custom_uniforms,
192            background_channel0_blend_mode,
193            auto_dim_under_text,
194            auto_dim_strength,
195        } = params;
196        match (enabled, shader_path) {
197            (true, Some(path)) => {
198                // Check if the shader path has changed
199                let path_changed = self.custom_shader_path.as_deref() != Some(path);
200
201                // If we already have a shader renderer and path hasn't changed, just update flags and textures
202                if let Some(renderer) = &mut self.custom_shader_renderer
203                    && !path_changed
204                {
205                    renderer.set_animation_enabled(animation_enabled);
206                    renderer.set_animation_speed(animation_speed);
207                    renderer.set_opacity(window_opacity);
208                    renderer.set_full_content_mode(full_content);
209                    renderer.set_brightness(brightness);
210                    renderer.set_auto_dim_under_text(auto_dim_under_text, auto_dim_strength);
211                    renderer.set_background_channel0_blend_mode(background_channel0_blend_mode);
212                    renderer.set_custom_uniform_values(custom_uniforms.clone());
213
214                    // Update channel textures (they may have changed even if shader path didn't)
215                    for (i, path) in channel_paths.iter().enumerate() {
216                        if let Err(e) = renderer.update_channel_texture(
217                            self.cell_renderer.device(),
218                            self.cell_renderer.queue(),
219                            (i + 1) as u8, // channel indices are 1-4
220                            path.as_deref(),
221                        ) {
222                            log::warn!("Failed to update channel {} texture: {}", i, e);
223                        }
224                    }
225
226                    // Update cubemap if provided
227                    if let Some(cubemap) = cubemap_path
228                        && let Err(e) = renderer.update_cubemap(
229                            self.cell_renderer.device(),
230                            self.cell_renderer.queue(),
231                            Some(cubemap),
232                        )
233                    {
234                        log::warn!("Failed to update cubemap: {}", e);
235                    }
236
237                    return Ok(());
238                }
239
240                let shader_path_full = par_term_config::Config::shader_path(path);
241                match CustomShaderRenderer::new(
242                    self.cell_renderer.device(),
243                    self.cell_renderer.queue(),
244                    crate::custom_shader_renderer::CustomShaderRendererConfig {
245                        surface_format: self.cell_renderer.surface_format(),
246                        shader_path: &shader_path_full,
247                        width: self.size.width,
248                        height: self.size.height,
249                        animation_enabled,
250                        animation_speed,
251                        window_opacity,
252                        full_content_mode: full_content,
253                        channel_paths,
254                        cubemap_path,
255                        custom_uniforms,
256                        background_channel0_blend_mode,
257                    },
258                ) {
259                    Ok(mut renderer) => {
260                        // Sync cell dimensions for cursor position calculation
261                        renderer.update_cell_dimensions(
262                            self.cell_renderer.cell_width(),
263                            self.cell_renderer.cell_height(),
264                            self.cell_renderer.window_padding(),
265                        );
266                        // Sync DPI scale factor for cursor sizing
267                        renderer.set_scale_factor(self.cell_renderer.scale_factor);
268                        // Apply brightness setting
269                        renderer.set_brightness(brightness);
270                        renderer.set_auto_dim_under_text(auto_dim_under_text, auto_dim_strength);
271                        // Sync keep_text_opaque from cell renderer
272                        renderer.set_keep_text_opaque(self.cell_renderer.keep_text_opaque());
273                        // Pass background color but don't activate solid color mode
274                        // Custom shaders handle their own background
275                        renderer.set_background_color(
276                            self.cell_renderer.solid_background_color(),
277                            false,
278                        );
279                        log::info!(
280                            "[SHADER] Custom shader enabled at runtime: {}",
281                            shader_path_full.display()
282                        );
283                        self.custom_shader_renderer = Some(renderer);
284                        self.custom_shader_path = Some(path.to_string());
285
286                        // When background shader is enabled, cursor shader should not have its own background
287                        self.sync_cursor_shader_background_state();
288
289                        self.dirty = true;
290                        Ok(())
291                    }
292                    Err(e) => {
293                        let error_msg = format!(
294                            "Failed to load shader '{}': {}",
295                            shader_path_full.display(),
296                            e
297                        );
298                        log::info!("[SHADER] ERROR: {}", error_msg);
299                        Err(error_msg)
300                    }
301                }
302            }
303            _ => {
304                if self.custom_shader_renderer.is_some() {
305                    log::info!("[SHADER] Custom shader disabled at runtime");
306                }
307                self.custom_shader_renderer = None;
308                self.custom_shader_path = None;
309
310                // When background shader is disabled, cursor shader should get its own background back
311                self.sync_cursor_shader_background_state();
312
313                self.dirty = true;
314                Ok(())
315            }
316        }
317    }
318
319    /// Set whether to use the background image as iChannel0 for the custom shader.
320    pub fn set_use_background_as_channel0(&mut self, use_background: bool) {
321        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
322            custom_shader
323                .update_use_background_as_channel0(self.cell_renderer.device(), use_background);
324            self.dirty = true;
325        }
326    }
327
328    /// Update the background texture for use as iChannel0 in shaders.
329    ///
330    /// Call this whenever the background image changes to sync the shader's
331    /// channel0 texture. Only has effect if use_background_as_channel0 is enabled.
332    pub fn sync_background_texture_to_shader(&mut self) {
333        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
334            let bg_texture = self.cell_renderer.get_background_as_channel_texture();
335            custom_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
336            self.dirty = true;
337        }
338    }
339
340    /// Update both the use_background_as_channel0 flag and sync the texture.
341    pub fn update_background_as_channel0(&mut self, use_background: bool) {
342        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
343            // Always sync the background texture first - it may have changed
344            let bg_texture = self.cell_renderer.get_background_as_channel_texture();
345            custom_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
346
347            // Then update the flag - this will recreate bind group if flag actually changed
348            custom_shader
349                .update_use_background_as_channel0(self.cell_renderer.device(), use_background);
350
351            self.dirty = true;
352        }
353    }
354
355    /// Update background as channel0 with solid color support.
356    ///
357    /// Handles the case where background_mode is Color and we need to
358    /// create a solid color texture to pass as iChannel0 instead of an image.
359    pub fn update_background_as_channel0_with_mode(
360        &mut self,
361        use_background: bool,
362        background_mode: par_term_config::BackgroundMode,
363        color: [u8; 3],
364    ) {
365        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
366            let bg_texture = match background_mode {
367                par_term_config::BackgroundMode::Default => {
368                    log::info!("update_background_as_channel0_with_mode: Default mode, no texture");
369                    None
370                }
371                par_term_config::BackgroundMode::Color => {
372                    log::info!(
373                        "update_background_as_channel0_with_mode: Color mode, creating solid color texture RGB({},{},{})",
374                        color[0],
375                        color[1],
376                        color[2]
377                    );
378                    Some(self.cell_renderer.get_solid_color_as_channel_texture(color))
379                }
380                par_term_config::BackgroundMode::Image => {
381                    let tex = self.cell_renderer.get_background_as_channel_texture();
382                    log::info!(
383                        "update_background_as_channel0_with_mode: Image mode, texture={}",
384                        if tex.is_some() { "Some" } else { "None" }
385                    );
386                    tex
387                }
388            };
389
390            let has_texture = bg_texture.is_some();
391            custom_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
392            custom_shader
393                .update_use_background_as_channel0(self.cell_renderer.device(), use_background);
394
395            log::info!(
396                "update_background_as_channel0_with_mode: use_background={}, has_texture={}",
397                use_background,
398                has_texture
399            );
400
401            self.dirty = true;
402        }
403    }
404}