Skip to main content

par_term_render/custom_shader_renderer/
textures.rs

1//! Channel texture management for custom shaders
2//!
3//! Provides loading and management of texture channels (iChannel0-3)
4//! that can be used by custom shaders alongside the terminal content (iChannel4).
5
6use anyhow::{Context, Result};
7use std::path::Path;
8use wgpu::*;
9
10/// A texture channel that can be bound to a custom shader
11pub struct ChannelTexture {
12    /// The GPU texture (kept alive to ensure view/sampler remain valid)
13    /// When using an external texture (e.g., background image), this is None
14    #[allow(dead_code)]
15    pub texture: Option<Texture>,
16    /// View for binding to shaders
17    pub view: TextureView,
18    /// Sampler for texture filtering
19    pub sampler: Sampler,
20    /// Texture width in pixels
21    pub width: u32,
22    /// Texture height in pixels
23    pub height: u32,
24}
25
26impl ChannelTexture {
27    /// Create a 1x1 transparent black placeholder texture
28    ///
29    /// This is used when no texture is configured for a channel,
30    /// ensuring the shader can still sample from it without errors.
31    pub fn placeholder(device: &Device, queue: &Queue) -> Self {
32        let texture = device.create_texture(&TextureDescriptor {
33            label: Some("Channel Placeholder Texture"),
34            size: Extent3d {
35                width: 1,
36                height: 1,
37                depth_or_array_layers: 1,
38            },
39            mip_level_count: 1,
40            sample_count: 1,
41            dimension: TextureDimension::D2,
42            format: TextureFormat::Rgba8UnormSrgb,
43            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
44            view_formats: &[],
45        });
46
47        // Write transparent black pixel
48        queue.write_texture(
49            TexelCopyTextureInfo {
50                texture: &texture,
51                mip_level: 0,
52                origin: Origin3d::ZERO,
53                aspect: TextureAspect::All,
54            },
55            &[0u8, 0, 0, 0], // RGBA: transparent black
56            TexelCopyBufferLayout {
57                offset: 0,
58                bytes_per_row: Some(4),
59                rows_per_image: Some(1),
60            },
61            Extent3d {
62                width: 1,
63                height: 1,
64                depth_or_array_layers: 1,
65            },
66        );
67
68        let view = texture.create_view(&TextureViewDescriptor::default());
69        let sampler = device.create_sampler(&SamplerDescriptor {
70            label: Some("Channel Placeholder Sampler"),
71            address_mode_u: AddressMode::ClampToEdge,
72            address_mode_v: AddressMode::ClampToEdge,
73            address_mode_w: AddressMode::ClampToEdge,
74            mag_filter: FilterMode::Linear,
75            min_filter: FilterMode::Linear,
76            mipmap_filter: FilterMode::Linear,
77            ..Default::default()
78        });
79
80        Self {
81            texture: Some(texture),
82            view,
83            sampler,
84            width: 1,
85            height: 1,
86        }
87    }
88
89    /// Create a ChannelTexture from an existing texture view and sampler.
90    ///
91    /// This is used when sharing a texture from another source (e.g., background image)
92    /// without creating a new copy. The caller is responsible for keeping the source
93    /// texture alive while this ChannelTexture is in use.
94    ///
95    /// # Arguments
96    /// * `view` - The texture view to use
97    /// * `sampler` - The sampler for texture filtering
98    /// * `width` - Texture width in pixels
99    /// * `height` - Texture height in pixels
100    pub fn from_view(view: TextureView, sampler: Sampler, width: u32, height: u32) -> Self {
101        Self {
102            texture: None,
103            view,
104            sampler,
105            width,
106            height,
107        }
108    }
109
110    /// Create a ChannelTexture from a view, sampler, and owned texture.
111    ///
112    /// This is used when creating a new texture that should be kept alive
113    /// by this ChannelTexture instance (e.g., solid color textures).
114    ///
115    /// # Arguments
116    /// * `view` - The texture view to use
117    /// * `sampler` - The sampler for texture filtering
118    /// * `width` - Texture width in pixels
119    /// * `height` - Texture height in pixels
120    /// * `texture` - The owned texture to keep alive
121    pub fn from_view_and_texture(
122        view: TextureView,
123        sampler: Sampler,
124        width: u32,
125        height: u32,
126        texture: Texture,
127    ) -> Self {
128        Self {
129            texture: Some(texture),
130            view,
131            sampler,
132            width,
133            height,
134        }
135    }
136
137    /// Load a texture from an image file
138    ///
139    /// Supports common image formats (PNG, JPEG, etc.) via the `image` crate.
140    ///
141    /// # Arguments
142    /// * `device` - The wgpu device
143    /// * `queue` - The wgpu queue
144    /// * `path` - Path to the image file
145    ///
146    /// # Returns
147    /// The loaded texture, or an error if loading fails
148    pub fn from_file(device: &Device, queue: &Queue, path: &Path) -> Result<Self> {
149        // Load image and convert to RGBA8
150        let img = image::open(path)
151            .with_context(|| format!("Failed to open image: {}", path.display()))?
152            .to_rgba8();
153
154        let (width, height) = img.dimensions();
155
156        // Create GPU texture
157        let texture = device.create_texture(&TextureDescriptor {
158            label: Some(&format!("Channel Texture: {}", path.display())),
159            size: Extent3d {
160                width,
161                height,
162                depth_or_array_layers: 1,
163            },
164            mip_level_count: 1,
165            sample_count: 1,
166            dimension: TextureDimension::D2,
167            format: TextureFormat::Rgba8UnormSrgb,
168            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
169            view_formats: &[],
170        });
171
172        // Upload image data to GPU
173        queue.write_texture(
174            TexelCopyTextureInfo {
175                texture: &texture,
176                mip_level: 0,
177                origin: Origin3d::ZERO,
178                aspect: TextureAspect::All,
179            },
180            &img,
181            TexelCopyBufferLayout {
182                offset: 0,
183                bytes_per_row: Some(4 * width),
184                rows_per_image: Some(height),
185            },
186            Extent3d {
187                width,
188                height,
189                depth_or_array_layers: 1,
190            },
191        );
192
193        let view = texture.create_view(&TextureViewDescriptor::default());
194
195        // Create sampler with wrapping for tiled textures
196        let sampler = device.create_sampler(&SamplerDescriptor {
197            label: Some(&format!("Channel Sampler: {}", path.display())),
198            address_mode_u: AddressMode::Repeat,
199            address_mode_v: AddressMode::Repeat,
200            address_mode_w: AddressMode::Repeat,
201            mag_filter: FilterMode::Linear,
202            min_filter: FilterMode::Linear,
203            mipmap_filter: FilterMode::Linear,
204            ..Default::default()
205        });
206
207        log::info!(
208            "Loaded channel texture: {} ({}x{})",
209            path.display(),
210            width,
211            height
212        );
213
214        Ok(Self {
215            texture: Some(texture),
216            view,
217            sampler,
218            width,
219            height,
220        })
221    }
222
223    /// Get the resolution as a vec4 [width, height, 1.0, 0.0]
224    ///
225    /// This format matches Shadertoy's iChannelResolution uniform.
226    pub fn resolution(&self) -> [f32; 4] {
227        [self.width as f32, self.height as f32, 1.0, 0.0]
228    }
229}
230
231/// Load channel textures from optional paths
232///
233/// # Arguments
234/// * `device` - The wgpu device
235/// * `queue` - The wgpu queue
236/// * `paths` - Array of 4 optional paths for iChannel0-3
237///
238/// # Returns
239/// Array of 4 ChannelTexture instances (placeholders for None paths)
240pub fn load_channel_textures(
241    device: &Device,
242    queue: &Queue,
243    paths: &[Option<std::path::PathBuf>; 4],
244) -> [ChannelTexture; 4] {
245    let load_or_placeholder = |path: &Option<std::path::PathBuf>, index: usize| -> ChannelTexture {
246        match path {
247            Some(p) => match ChannelTexture::from_file(device, queue, p) {
248                Ok(tex) => tex,
249                Err(e) => {
250                    log::error!(
251                        "Failed to load iChannel{} texture '{}': {}",
252                        index,
253                        p.display(),
254                        e
255                    );
256                    ChannelTexture::placeholder(device, queue)
257                }
258            },
259            None => ChannelTexture::placeholder(device, queue),
260        }
261    };
262
263    [
264        load_or_placeholder(&paths[0], 0),
265        load_or_placeholder(&paths[1], 1),
266        load_or_placeholder(&paths[2], 2),
267        load_or_placeholder(&paths[3], 3),
268    ]
269}