par_term/
graphics_renderer.rs

1use anyhow::Result;
2use std::collections::HashMap;
3use wgpu::*;
4
5/// Instance data for a single sixel graphic
6#[repr(C)]
7#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
8struct SixelInstance {
9    position: [f32; 2],   // Screen position (normalized 0-1)
10    tex_coords: [f32; 4], // Texture coordinates (x, y, w, h) - normalized 0-1
11    size: [f32; 2],       // Image size in screen space (normalized 0-1)
12    alpha: f32,           // Global alpha multiplier
13    _padding: f32,        // Padding to align to 16 bytes
14}
15
16/// Metadata for a cached sixel texture
17struct SixelTextureInfo {
18    #[allow(dead_code)]
19    texture: Texture,
20    #[allow(dead_code)]
21    view: TextureView,
22    bind_group: BindGroup,
23    #[allow(dead_code)]
24    width: u32,
25    #[allow(dead_code)]
26    height: u32,
27}
28
29/// Graphics renderer for sixel images
30pub struct GraphicsRenderer {
31    // Rendering pipeline
32    pipeline: RenderPipeline,
33    bind_group_layout: BindGroupLayout,
34    sampler: Sampler,
35
36    // Instance buffer
37    instance_buffer: Buffer,
38    instance_capacity: usize,
39
40    // Texture cache: maps sixel ID to texture info
41    texture_cache: HashMap<u64, SixelTextureInfo>,
42
43    // Cell dimensions for positioning
44    cell_width: f32,
45    cell_height: f32,
46    window_padding: f32,
47
48    // Surface format for pipeline compatibility
49    #[allow(dead_code)]
50    surface_format: TextureFormat,
51}
52
53impl GraphicsRenderer {
54    /// Create a new graphics renderer
55    pub fn new(
56        device: &Device,
57        surface_format: TextureFormat,
58        cell_width: f32,
59        cell_height: f32,
60        window_padding: f32,
61    ) -> Result<Self> {
62        // Create bind group layout for sixel textures
63        let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
64            label: Some("Sixel Bind Group Layout"),
65            entries: &[
66                // Sixel texture
67                BindGroupLayoutEntry {
68                    binding: 0,
69                    visibility: ShaderStages::FRAGMENT,
70                    ty: BindingType::Texture {
71                        sample_type: TextureSampleType::Float { filterable: true },
72                        view_dimension: TextureViewDimension::D2,
73                        multisampled: false,
74                    },
75                    count: None,
76                },
77                // Sampler
78                BindGroupLayoutEntry {
79                    binding: 1,
80                    visibility: ShaderStages::FRAGMENT,
81                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
82                    count: None,
83                },
84            ],
85        });
86
87        // Create sampler
88        let sampler = device.create_sampler(&SamplerDescriptor {
89            label: Some("Sixel Sampler"),
90            address_mode_u: AddressMode::ClampToEdge,
91            address_mode_v: AddressMode::ClampToEdge,
92            address_mode_w: AddressMode::ClampToEdge,
93            mag_filter: FilterMode::Linear,
94            min_filter: FilterMode::Linear,
95            mipmap_filter: FilterMode::Nearest,
96            ..Default::default()
97        });
98
99        // Create rendering pipeline
100        let pipeline = Self::create_pipeline(device, surface_format, &bind_group_layout)?;
101
102        // Create instance buffer (initial capacity for 32 images)
103        let initial_capacity = 32;
104        let instance_buffer = device.create_buffer(&BufferDescriptor {
105            label: Some("Sixel Instance Buffer"),
106            size: (initial_capacity * std::mem::size_of::<SixelInstance>()) as u64,
107            usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
108            mapped_at_creation: false,
109        });
110
111        Ok(Self {
112            pipeline,
113            bind_group_layout,
114            sampler,
115            instance_buffer,
116            instance_capacity: initial_capacity,
117            texture_cache: HashMap::new(),
118            cell_width,
119            cell_height,
120            window_padding,
121            surface_format,
122        })
123    }
124
125    /// Create the sixel rendering pipeline
126    fn create_pipeline(
127        device: &Device,
128        format: TextureFormat,
129        bind_group_layout: &BindGroupLayout,
130    ) -> Result<RenderPipeline> {
131        let shader = device.create_shader_module(ShaderModuleDescriptor {
132            label: Some("Sixel Shader"),
133            source: ShaderSource::Wgsl(include_str!("shaders/sixel.wgsl").into()),
134        });
135
136        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
137            label: Some("Sixel Pipeline Layout"),
138            bind_group_layouts: &[bind_group_layout],
139            push_constant_ranges: &[],
140        });
141
142        Ok(device.create_render_pipeline(&RenderPipelineDescriptor {
143            label: Some("Sixel Pipeline"),
144            layout: Some(&pipeline_layout),
145            vertex: VertexState {
146                module: &shader,
147                entry_point: Some("vs_main"),
148                buffers: &[VertexBufferLayout {
149                    array_stride: std::mem::size_of::<SixelInstance>() as u64,
150                    step_mode: VertexStepMode::Instance,
151                    attributes: &vertex_attr_array![
152                        0 => Float32x2,  // position
153                        1 => Float32x4,  // tex_coords
154                        2 => Float32x2,  // size
155                        3 => Float32,    // alpha
156                    ],
157                }],
158                compilation_options: Default::default(),
159            },
160            fragment: Some(FragmentState {
161                module: &shader,
162                entry_point: Some("fs_main"),
163                targets: &[Some(ColorTargetState {
164                    format,
165                    blend: Some(BlendState::ALPHA_BLENDING),
166                    write_mask: ColorWrites::ALL,
167                })],
168                compilation_options: Default::default(),
169            }),
170            primitive: PrimitiveState {
171                topology: PrimitiveTopology::TriangleStrip,
172                ..Default::default()
173            },
174            depth_stencil: None,
175            multisample: MultisampleState::default(),
176            multiview: None,
177            cache: None,
178        }))
179    }
180
181    /// Create or get a cached texture for a sixel graphic
182    ///
183    /// # Arguments
184    /// * `device` - WGPU device for creating textures
185    /// * `queue` - WGPU queue for writing texture data
186    /// * `id` - Unique identifier for this sixel graphic
187    /// * `rgba_data` - RGBA pixel data (width * height * 4 bytes)
188    /// * `width` - Image width in pixels
189    /// * `height` - Image height in pixels
190    pub fn get_or_create_texture(
191        &mut self,
192        device: &Device,
193        queue: &Queue,
194        id: u64,
195        rgba_data: &[u8],
196        width: u32,
197        height: u32,
198    ) -> Result<()> {
199        // Check if texture already exists in cache
200        // For animations, we need to update the texture data even if it exists
201        if let Some(tex_info) = self.texture_cache.get(&id) {
202            // Texture exists - update it if the data might have changed
203            // Validate data size
204            let expected_size = (width * height * 4) as usize;
205            if rgba_data.len() != expected_size {
206                return Err(anyhow::anyhow!(
207                    "Invalid RGBA data size: expected {}, got {}",
208                    expected_size,
209                    rgba_data.len()
210                ));
211            }
212
213            // Update existing texture with new pixel data (for animations)
214            queue.write_texture(
215                TexelCopyTextureInfo {
216                    texture: &tex_info.texture,
217                    mip_level: 0,
218                    origin: Origin3d::ZERO,
219                    aspect: TextureAspect::All,
220                },
221                rgba_data,
222                TexelCopyBufferLayout {
223                    offset: 0,
224                    bytes_per_row: Some(4 * width),
225                    rows_per_image: Some(height),
226                },
227                Extent3d {
228                    width,
229                    height,
230                    depth_or_array_layers: 1,
231                },
232            );
233
234            return Ok(());
235        }
236
237        // Validate data size
238        let expected_size = (width * height * 4) as usize;
239        if rgba_data.len() != expected_size {
240            return Err(anyhow::anyhow!(
241                "Invalid RGBA data size: expected {}, got {}",
242                expected_size,
243                rgba_data.len()
244            ));
245        }
246
247        // Create texture
248        let texture = device.create_texture(&TextureDescriptor {
249            label: Some(&format!("Sixel Texture {}", id)),
250            size: Extent3d {
251                width,
252                height,
253                depth_or_array_layers: 1,
254            },
255            mip_level_count: 1,
256            sample_count: 1,
257            dimension: TextureDimension::D2,
258            format: TextureFormat::Rgba8Unorm,
259            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
260            view_formats: &[],
261        });
262
263        // Write RGBA data to texture
264        queue.write_texture(
265            TexelCopyTextureInfo {
266                texture: &texture,
267                mip_level: 0,
268                origin: Origin3d::ZERO,
269                aspect: TextureAspect::All,
270            },
271            rgba_data,
272            TexelCopyBufferLayout {
273                offset: 0,
274                bytes_per_row: Some(4 * width),
275                rows_per_image: Some(height),
276            },
277            Extent3d {
278                width,
279                height,
280                depth_or_array_layers: 1,
281            },
282        );
283
284        let view = texture.create_view(&TextureViewDescriptor::default());
285
286        // Create bind group for this texture
287        let bind_group = device.create_bind_group(&BindGroupDescriptor {
288            label: Some(&format!("Sixel Bind Group {}", id)),
289            layout: &self.bind_group_layout,
290            entries: &[
291                BindGroupEntry {
292                    binding: 0,
293                    resource: BindingResource::TextureView(&view),
294                },
295                BindGroupEntry {
296                    binding: 1,
297                    resource: BindingResource::Sampler(&self.sampler),
298                },
299            ],
300        });
301
302        // Cache texture info
303        self.texture_cache.insert(
304            id,
305            SixelTextureInfo {
306                texture,
307                view,
308                bind_group,
309                width,
310                height,
311            },
312        );
313
314        debug_log!(
315            "GRAPHICS",
316            "Created sixel texture: id={}, size={}x{}, cache_size={}",
317            id,
318            width,
319            height,
320            self.texture_cache.len()
321        );
322
323        Ok(())
324    }
325
326    /// Render sixel graphics
327    ///
328    /// # Arguments
329    /// * `device` - WGPU device for creating buffers
330    /// * `queue` - WGPU queue for writing buffer data
331    /// * `render_pass` - Active render pass to render into
332    /// * `graphics` - Slice of sixel graphics to render with their positions
333    ///   Each tuple contains: (id, row, col, width_in_cells, height_in_cells, alpha, scroll_offset_rows)
334    /// * `window_width` - Window width in pixels
335    /// * `window_height` - Window height in pixels
336    pub fn render(
337        &mut self,
338        device: &Device,
339        queue: &Queue,
340        render_pass: &mut RenderPass,
341        graphics: &[(u64, isize, usize, usize, usize, f32, usize)],
342        window_width: f32,
343        window_height: f32,
344    ) -> Result<()> {
345        if graphics.is_empty() {
346            return Ok(());
347        }
348
349        // Build instance data
350        let mut instances = Vec::with_capacity(graphics.len());
351        for &(id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) in graphics {
352            // Check if texture exists
353            if let Some(tex_info) = self.texture_cache.get(&id) {
354                // Calculate screen position (normalized 0-1, origin top-left)
355                let x = (self.window_padding + col as f32 * self.cell_width) / window_width;
356                let y = (self.window_padding + row as f32 * self.cell_height) / window_height;
357
358                // Calculate texture V offset for scrolled graphics
359                // scroll_offset_rows = terminal rows scrolled off top
360                // Each terminal row = cell_height pixels
361                let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
362                    let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
363                    (pixels_scrolled / tex_info.height as f32).min(0.99)
364                } else {
365                    0.0
366                };
367                let tex_v_height = 1.0 - tex_v_start;
368
369                // For graphics, use actual texture pixel dimensions for aspect ratio preservation
370                // Rather than converting pixels→cells→pixels (which distorts non-square cells)
371                let visible_height_pixels = if scroll_offset_rows > 0 {
372                    // Account for scrolled content by reducing visible height
373                    (tex_info.height as f32 * tex_v_height).max(1.0)
374                } else {
375                    tex_info.height as f32
376                };
377
378                // Calculate size in screen space (normalized 0-1) using actual pixel dimensions
379                // This preserves aspect ratio regardless of cell dimensions
380                let width = tex_info.width as f32 / window_width;
381                let height = visible_height_pixels / window_height;
382
383                instances.push(SixelInstance {
384                    position: [x, y],
385                    tex_coords: [0.0, tex_v_start, 1.0, tex_v_height], // Crop from top
386                    size: [width, height],
387                    alpha,
388                    _padding: 0.0,
389                });
390            }
391        }
392
393        if instances.is_empty() {
394            return Ok(());
395        }
396
397        // Debug: log sixel rendering
398        debug_log!(
399            "GRAPHICS",
400            "Rendering {} sixel graphics (from {} total graphics provided)",
401            instances.len(),
402            graphics.len()
403        );
404
405        // Resize instance buffer if needed
406        let required_capacity = instances.len();
407        if required_capacity > self.instance_capacity {
408            let new_capacity = (required_capacity * 2).max(32);
409            self.instance_buffer = device.create_buffer(&BufferDescriptor {
410                label: Some("Sixel Instance Buffer"),
411                size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
412                usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
413                mapped_at_creation: false,
414            });
415            self.instance_capacity = new_capacity;
416        }
417
418        // Write instance data to buffer
419        queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
420
421        // Set pipeline
422        render_pass.set_pipeline(&self.pipeline);
423
424        // Render each graphic with its specific bind group
425        render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
426
427        // Use separate counter for instance index since we filtered out graphics without textures
428        let mut instance_idx = 0u32;
429        for &(id, _, _, _, _, _, _) in graphics {
430            if let Some(tex_info) = self.texture_cache.get(&id) {
431                render_pass.set_bind_group(0, &tex_info.bind_group, &[]);
432                render_pass.draw(0..4, instance_idx..(instance_idx + 1));
433                instance_idx += 1;
434            }
435        }
436
437        Ok(())
438    }
439
440    /// Remove a texture from the cache
441    #[allow(dead_code)]
442    pub fn remove_texture(&mut self, id: u64) {
443        self.texture_cache.remove(&id);
444    }
445
446    /// Clear all cached textures
447    #[allow(dead_code)]
448    pub fn clear_cache(&mut self) {
449        self.texture_cache.clear();
450    }
451
452    /// Get the number of cached textures
453    #[allow(dead_code)]
454    pub fn cache_size(&self) -> usize {
455        self.texture_cache.len()
456    }
457
458    /// Update cell dimensions (called when window is resized)
459    pub fn update_cell_dimensions(
460        &mut self,
461        cell_width: f32,
462        cell_height: f32,
463        window_padding: f32,
464    ) {
465        self.cell_width = cell_width;
466        self.cell_height = cell_height;
467        self.window_padding = window_padding;
468    }
469}