1use crate::error::RenderError;
2use crate::gpu_utils;
3use par_term_config::ImageScalingMode;
4use std::collections::HashMap;
5use std::time::Instant;
6use wgpu::*;
7
8const MAX_TEXTURE_CACHE_SIZE: usize = 100;
11
12const INITIAL_GRAPHICS_INSTANCE_CAPACITY: usize = 32;
15
16#[repr(C)]
18#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
19struct SixelInstance {
20 position: [f32; 2], tex_coords: [f32; 4], size: [f32; 2], alpha: f32, _padding: f32, }
26
27#[derive(Debug, Clone, Copy)]
29pub struct PaneRenderGeometry {
30 pub window_width: f32,
31 pub window_height: f32,
32 pub pane_origin_x: f32,
33 pub pane_origin_y: f32,
34}
35
36#[derive(Debug, Clone, Copy)]
42pub struct GraphicRenderInfo {
43 pub id: u64,
45 pub screen_row: isize,
47 pub col: usize,
49 pub width_cells: usize,
51 pub height_cells: usize,
53 pub alpha: f32,
55 pub scroll_offset_rows: usize,
57}
58
59struct SixelTextureInfo {
61 texture: Texture,
62 #[allow(dead_code)] view: TextureView,
64 bind_group: BindGroup,
65 width: u32,
66 height: u32,
67}
68
69struct CachedTexture {
71 texture: SixelTextureInfo,
72 last_used: Instant,
74}
75
76pub struct GraphicsRenderer {
78 pipeline: RenderPipeline,
80 bind_group_layout: BindGroupLayout,
81 sampler: Sampler,
82
83 instance_buffer: Buffer,
85 instance_capacity: usize,
86
87 texture_cache: HashMap<u64, CachedTexture>,
89
90 cell_width: f32,
92 cell_height: f32,
93 window_padding: f32,
94 content_offset_y: f32,
96 content_offset_x: f32,
98
99 preserve_aspect_ratio: bool,
101}
102
103impl GraphicsRenderer {
104 pub fn new(
106 device: &Device,
107 surface_format: TextureFormat,
108 cell_width: f32,
109 cell_height: f32,
110 window_padding: f32,
111 scaling_mode: ImageScalingMode,
112 preserve_aspect_ratio: bool,
113 ) -> Result<Self, RenderError> {
114 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
116 label: Some("Sixel Bind Group Layout"),
117 entries: &[
118 BindGroupLayoutEntry {
120 binding: 0,
121 visibility: ShaderStages::FRAGMENT,
122 ty: BindingType::Texture {
123 sample_type: TextureSampleType::Float { filterable: true },
124 view_dimension: TextureViewDimension::D2,
125 multisampled: false,
126 },
127 count: None,
128 },
129 BindGroupLayoutEntry {
131 binding: 1,
132 visibility: ShaderStages::FRAGMENT,
133 ty: BindingType::Sampler(SamplerBindingType::Filtering),
134 count: None,
135 },
136 ],
137 });
138
139 let sampler = gpu_utils::create_sampler_with_filter(
141 device,
142 scaling_mode.to_filter_mode(),
143 Some("Sixel Sampler"),
144 );
145
146 let pipeline = Self::create_pipeline(device, surface_format, &bind_group_layout)?;
148
149 let initial_capacity = INITIAL_GRAPHICS_INSTANCE_CAPACITY;
151 let instance_buffer = device.create_buffer(&BufferDescriptor {
152 label: Some("Sixel Instance Buffer"),
153 size: (initial_capacity * std::mem::size_of::<SixelInstance>()) as u64,
154 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
155 mapped_at_creation: false,
156 });
157
158 Ok(Self {
159 pipeline,
160 bind_group_layout,
161 sampler,
162 instance_buffer,
163 instance_capacity: initial_capacity,
164 texture_cache: HashMap::new(),
165 cell_width,
166 cell_height,
167 window_padding,
168 content_offset_y: 0.0,
169 content_offset_x: 0.0,
170 preserve_aspect_ratio,
171 })
172 }
173
174 fn create_pipeline(
176 device: &Device,
177 format: TextureFormat,
178 bind_group_layout: &BindGroupLayout,
179 ) -> Result<RenderPipeline, RenderError> {
180 let shader = device.create_shader_module(ShaderModuleDescriptor {
181 label: Some("Sixel Shader"),
182 source: ShaderSource::Wgsl(include_str!("shaders/sixel.wgsl").into()),
183 });
184
185 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
186 label: Some("Sixel Pipeline Layout"),
187 bind_group_layouts: &[bind_group_layout],
188 push_constant_ranges: &[],
189 });
190
191 Ok(device.create_render_pipeline(&RenderPipelineDescriptor {
192 label: Some("Sixel Pipeline"),
193 layout: Some(&pipeline_layout),
194 vertex: VertexState {
195 module: &shader,
196 entry_point: Some("vs_main"),
197 buffers: &[VertexBufferLayout {
198 array_stride: std::mem::size_of::<SixelInstance>() as u64,
199 step_mode: VertexStepMode::Instance,
200 attributes: &vertex_attr_array![
201 0 => Float32x2, 1 => Float32x4, 2 => Float32x2, 3 => Float32, ],
206 }],
207 compilation_options: Default::default(),
208 },
209 fragment: Some(FragmentState {
210 module: &shader,
211 entry_point: Some("fs_main"),
212 targets: &[Some(ColorTargetState {
213 format,
214 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
216 write_mask: ColorWrites::ALL,
217 })],
218 compilation_options: Default::default(),
219 }),
220 primitive: PrimitiveState {
221 topology: PrimitiveTopology::TriangleStrip,
222 ..Default::default()
223 },
224 depth_stencil: None,
225 multisample: MultisampleState::default(),
226 multiview: None,
227 cache: None,
228 }))
229 }
230
231 pub fn get_or_create_texture(
241 &mut self,
242 device: &Device,
243 queue: &Queue,
244 id: u64,
245 rgba_data: &[u8],
246 width: u32,
247 height: u32,
248 ) -> Result<(), RenderError> {
249 if let Some(cached) = self.texture_cache.get_mut(&id) {
252 cached.last_used = Instant::now();
254
255 let expected_size = (width * height * 4) as usize;
258 if rgba_data.len() != expected_size {
259 return Err(RenderError::InvalidTextureData {
260 expected: expected_size,
261 actual: rgba_data.len(),
262 });
263 }
264
265 queue.write_texture(
267 TexelCopyTextureInfo {
268 texture: &cached.texture.texture,
269 mip_level: 0,
270 origin: Origin3d::ZERO,
271 aspect: TextureAspect::All,
272 },
273 rgba_data,
274 TexelCopyBufferLayout {
275 offset: 0,
276 bytes_per_row: Some(4 * width),
277 rows_per_image: Some(height),
278 },
279 Extent3d {
280 width,
281 height,
282 depth_or_array_layers: 1,
283 },
284 );
285
286 return Ok(());
287 }
288
289 let expected_size = (width * height * 4) as usize;
291 if rgba_data.len() != expected_size {
292 return Err(RenderError::InvalidTextureData {
293 expected: expected_size,
294 actual: rgba_data.len(),
295 });
296 }
297
298 if self.texture_cache.len() >= MAX_TEXTURE_CACHE_SIZE
300 && let Some((&lru_id, _)) = self
301 .texture_cache
302 .iter()
303 .min_by_key(|(_, cached)| cached.last_used)
304 {
305 log::debug!(
306 "[GRAPHICS] Evicting LRU texture: id={}, cache_size={}",
307 lru_id,
308 self.texture_cache.len()
309 );
310 self.texture_cache.remove(&lru_id);
311 }
312
313 let texture = device.create_texture(&TextureDescriptor {
315 label: Some(&format!("Sixel Texture {}", id)),
316 size: Extent3d {
317 width,
318 height,
319 depth_or_array_layers: 1,
320 },
321 mip_level_count: 1,
322 sample_count: 1,
323 dimension: TextureDimension::D2,
324 format: TextureFormat::Rgba8Unorm,
325 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
326 view_formats: &[],
327 });
328
329 queue.write_texture(
331 TexelCopyTextureInfo {
332 texture: &texture,
333 mip_level: 0,
334 origin: Origin3d::ZERO,
335 aspect: TextureAspect::All,
336 },
337 rgba_data,
338 TexelCopyBufferLayout {
339 offset: 0,
340 bytes_per_row: Some(4 * width),
341 rows_per_image: Some(height),
342 },
343 Extent3d {
344 width,
345 height,
346 depth_or_array_layers: 1,
347 },
348 );
349
350 let view = texture.create_view(&TextureViewDescriptor::default());
351
352 let bind_group = device.create_bind_group(&BindGroupDescriptor {
354 label: Some(&format!("Sixel Bind Group {}", id)),
355 layout: &self.bind_group_layout,
356 entries: &[
357 BindGroupEntry {
358 binding: 0,
359 resource: BindingResource::TextureView(&view),
360 },
361 BindGroupEntry {
362 binding: 1,
363 resource: BindingResource::Sampler(&self.sampler),
364 },
365 ],
366 });
367
368 self.texture_cache.insert(
370 id,
371 CachedTexture {
372 texture: SixelTextureInfo {
373 texture,
374 view,
375 bind_group,
376 width,
377 height,
378 },
379 last_used: Instant::now(),
380 },
381 );
382
383 log::debug!(
384 "[GRAPHICS] Created sixel texture: id={}, size={}x{}, cache_size={}/{}",
385 id,
386 width,
387 height,
388 self.texture_cache.len(),
389 MAX_TEXTURE_CACHE_SIZE
390 );
391
392 Ok(())
393 }
394
395 pub fn render(
405 &mut self,
406 device: &Device,
407 queue: &Queue,
408 render_pass: &mut RenderPass,
409 graphics: &[GraphicRenderInfo],
410 window_width: f32,
411 window_height: f32,
412 ) -> Result<(), RenderError> {
413 if graphics.is_empty() {
414 return Ok(());
415 }
416
417 let mut instances = Vec::with_capacity(graphics.len());
419 for g in graphics {
420 let (id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) = (
421 g.id,
422 g.screen_row,
423 g.col,
424 g.width_cells,
425 g.height_cells,
426 g.alpha,
427 g.scroll_offset_rows,
428 );
429 if let Some(cached) = self.texture_cache.get_mut(&id) {
431 cached.last_used = Instant::now();
432 let tex_info = &cached.texture;
433
434 let adjusted_row = row + scroll_offset_rows as isize;
439 let x =
440 (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
441 / window_width;
442 let y = (self.window_padding
443 + self.content_offset_y
444 + adjusted_row as f32 * self.cell_height)
445 / window_height;
446
447 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
451 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
452 (pixels_scrolled / tex_info.height as f32).min(0.99)
453 } else {
454 0.0
455 };
456 let tex_v_height = 1.0 - tex_v_start;
457
458 let (width, height) = if self.preserve_aspect_ratio {
460 let visible_height_pixels = if scroll_offset_rows > 0 {
463 (tex_info.height as f32 * tex_v_height).max(1.0)
464 } else {
465 tex_info.height as f32
466 };
467 (
468 tex_info.width as f32 / window_width,
469 visible_height_pixels / window_height,
470 )
471 } else {
472 let cell_w = _width_cells as f32 * self.cell_width / window_width;
474 let visible_cell_rows = if scroll_offset_rows > 0 {
475 (_height_cells as f32 * tex_v_height).max(0.0)
476 } else {
477 _height_cells as f32
478 };
479 let cell_h = visible_cell_rows * self.cell_height / window_height;
480 (cell_w, cell_h)
481 };
482
483 instances.push(SixelInstance {
484 position: [x, y],
485 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height], size: [width, height],
487 alpha,
488 _padding: 0.0,
489 });
490 }
491 }
492
493 if instances.is_empty() {
494 return Ok(());
495 }
496
497 log::debug!(
499 "[GRAPHICS] Rendering {} sixel graphics (from {} total graphics provided)",
500 instances.len(),
501 graphics.len()
502 );
503
504 let required_capacity = instances.len();
506 if required_capacity > self.instance_capacity {
507 let new_capacity = (required_capacity * 2).max(32);
508 self.instance_buffer = device.create_buffer(&BufferDescriptor {
509 label: Some("Sixel Instance Buffer"),
510 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
511 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
512 mapped_at_creation: false,
513 });
514 self.instance_capacity = new_capacity;
515 }
516
517 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
519
520 render_pass.set_pipeline(&self.pipeline);
522
523 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
525
526 let mut instance_idx = 0u32;
528 for g in graphics {
529 if let Some(cached) = self.texture_cache.get(&g.id) {
530 render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
531 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
532 instance_idx += 1;
533 }
534 }
535
536 Ok(())
537 }
538
539 pub fn render_for_pane(
555 &mut self,
556 device: &Device,
557 queue: &Queue,
558 render_pass: &mut RenderPass,
559 graphics: &[GraphicRenderInfo],
560 pane_geometry: PaneRenderGeometry,
561 ) -> Result<(), RenderError> {
562 let PaneRenderGeometry {
563 window_width,
564 window_height,
565 pane_origin_x,
566 pane_origin_y,
567 } = pane_geometry;
568 if graphics.is_empty() {
569 return Ok(());
570 }
571
572 let mut instances = Vec::with_capacity(graphics.len());
574 for g in graphics {
575 let (id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) = (
576 g.id,
577 g.screen_row,
578 g.col,
579 g.width_cells,
580 g.height_cells,
581 g.alpha,
582 g.scroll_offset_rows,
583 );
584 if let Some(cached) = self.texture_cache.get_mut(&id) {
586 cached.last_used = Instant::now();
587 let tex_info = &cached.texture;
588
589 let adjusted_row = row + scroll_offset_rows as isize;
591 let x = (pane_origin_x + col as f32 * self.cell_width) / window_width;
592 let y = (pane_origin_y + adjusted_row as f32 * self.cell_height) / window_height;
593
594 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
596 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
597 (pixels_scrolled / tex_info.height as f32).min(0.99)
598 } else {
599 0.0
600 };
601 let tex_v_height = 1.0 - tex_v_start;
602
603 let (width, height) = if self.preserve_aspect_ratio {
605 let visible_height_pixels = if scroll_offset_rows > 0 {
606 (tex_info.height as f32 * tex_v_height).max(1.0)
607 } else {
608 tex_info.height as f32
609 };
610 (
611 tex_info.width as f32 / window_width,
612 visible_height_pixels / window_height,
613 )
614 } else {
615 let cell_w = _width_cells as f32 * self.cell_width / window_width;
616 let visible_cell_rows = if scroll_offset_rows > 0 {
617 (_height_cells as f32 * tex_v_height).max(0.0)
618 } else {
619 _height_cells as f32
620 };
621 let cell_h = visible_cell_rows * self.cell_height / window_height;
622 (cell_w, cell_h)
623 };
624
625 instances.push(SixelInstance {
626 position: [x, y],
627 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height],
628 size: [width, height],
629 alpha,
630 _padding: 0.0,
631 });
632 }
633 }
634
635 if instances.is_empty() {
636 return Ok(());
637 }
638
639 let required_capacity = instances.len();
641 if required_capacity > self.instance_capacity {
642 let new_capacity = (required_capacity * 2).max(32);
643 self.instance_buffer = device.create_buffer(&BufferDescriptor {
644 label: Some("Sixel Instance Buffer"),
645 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
646 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
647 mapped_at_creation: false,
648 });
649 self.instance_capacity = new_capacity;
650 }
651
652 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
654
655 render_pass.set_pipeline(&self.pipeline);
657 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
658
659 let mut instance_idx = 0u32;
660 for g in graphics {
661 if let Some(cached) = self.texture_cache.get(&g.id) {
662 render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
663 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
664 instance_idx += 1;
665 }
666 }
667
668 Ok(())
669 }
670
671 pub fn remove_texture(&mut self, id: u64) {
673 self.texture_cache.remove(&id);
674 }
675
676 pub fn clear_cache(&mut self) {
678 self.texture_cache.clear();
679 }
680
681 pub fn cache_size(&self) -> usize {
683 self.texture_cache.len()
684 }
685
686 pub fn update_cell_dimensions(
688 &mut self,
689 cell_width: f32,
690 cell_height: f32,
691 window_padding: f32,
692 ) {
693 self.cell_width = cell_width;
694 self.cell_height = cell_height;
695 self.window_padding = window_padding;
696 }
697
698 pub fn set_content_offset_y(&mut self, offset: f32) {
700 self.content_offset_y = offset;
701 }
702
703 pub fn set_content_offset_x(&mut self, offset: f32) {
705 self.content_offset_x = offset;
706 }
707
708 pub fn set_preserve_aspect_ratio(&mut self, preserve: bool) {
710 self.preserve_aspect_ratio = preserve;
711 }
712
713 pub fn update_scaling_mode(&mut self, device: &Device, scaling_mode: ImageScalingMode) {
718 self.sampler = gpu_utils::create_sampler_with_filter(
719 device,
720 scaling_mode.to_filter_mode(),
721 Some("Sixel Sampler"),
722 );
723 self.texture_cache.clear();
725 }
726}