1use crate::error::RenderError;
10use crate::gpu_utils;
11use crate::wgpu_conversions::ImageScalingModeWgpu;
12use par_term_config::ImageScalingMode;
13use std::collections::HashMap;
14use std::time::Instant;
15use wgpu::*;
16
17const MAX_TEXTURE_CACHE_SIZE: usize = 100;
20
21const INITIAL_GRAPHICS_INSTANCE_CAPACITY: usize = 32;
24
25#[repr(C)]
27#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
28struct SixelInstance {
29 position: [f32; 2], tex_coords: [f32; 4], size: [f32; 2], alpha: f32, _padding: f32, }
35
36#[derive(Debug, Clone, Copy)]
38pub struct PaneRenderGeometry {
39 pub window_width: f32,
40 pub window_height: f32,
41 pub pane_origin_x: f32,
42 pub pane_origin_y: f32,
43}
44
45#[derive(Debug, Clone, Copy)]
51pub struct GraphicRenderInfo {
52 pub id: u64,
54 pub screen_row: isize,
56 pub col: usize,
58 pub width_cells: usize,
60 pub height_cells: usize,
62 pub alpha: f32,
64 pub scroll_offset_rows: usize,
66}
67
68struct SixelTextureInfo {
70 texture: Texture,
71 #[allow(dead_code)] view: TextureView,
73 bind_group: BindGroup,
74 width: u32,
75 height: u32,
76}
77
78struct CachedTexture {
80 texture: SixelTextureInfo,
81 last_used: Instant,
83}
84
85pub struct GraphicsRenderer {
87 pipeline: RenderPipeline,
89 bind_group_layout: BindGroupLayout,
90 sampler: Sampler,
91
92 instance_buffer: Buffer,
94 instance_capacity: usize,
95
96 texture_cache: HashMap<u64, CachedTexture>,
98
99 cell_width: f32,
101 cell_height: f32,
102 window_padding: f32,
103 content_offset_y: f32,
105 content_offset_x: f32,
107
108 preserve_aspect_ratio: bool,
110}
111
112impl GraphicsRenderer {
113 pub fn new(
115 device: &Device,
116 surface_format: TextureFormat,
117 cell_width: f32,
118 cell_height: f32,
119 window_padding: f32,
120 scaling_mode: ImageScalingMode,
121 preserve_aspect_ratio: bool,
122 ) -> Result<Self, RenderError> {
123 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
125 label: Some("Sixel Bind Group Layout"),
126 entries: &[
127 BindGroupLayoutEntry {
129 binding: 0,
130 visibility: ShaderStages::FRAGMENT,
131 ty: BindingType::Texture {
132 sample_type: TextureSampleType::Float { filterable: true },
133 view_dimension: TextureViewDimension::D2,
134 multisampled: false,
135 },
136 count: None,
137 },
138 BindGroupLayoutEntry {
140 binding: 1,
141 visibility: ShaderStages::FRAGMENT,
142 ty: BindingType::Sampler(SamplerBindingType::Filtering),
143 count: None,
144 },
145 ],
146 });
147
148 let sampler = gpu_utils::create_sampler_with_filter(
150 device,
151 scaling_mode.to_filter_mode(),
152 Some("Sixel Sampler"),
153 );
154
155 let pipeline = Self::create_pipeline(device, surface_format, &bind_group_layout)?;
157
158 let initial_capacity = INITIAL_GRAPHICS_INSTANCE_CAPACITY;
160 let instance_buffer = device.create_buffer(&BufferDescriptor {
161 label: Some("Sixel Instance Buffer"),
162 size: (initial_capacity * std::mem::size_of::<SixelInstance>()) as u64,
163 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
164 mapped_at_creation: false,
165 });
166
167 Ok(Self {
168 pipeline,
169 bind_group_layout,
170 sampler,
171 instance_buffer,
172 instance_capacity: initial_capacity,
173 texture_cache: HashMap::new(),
174 cell_width,
175 cell_height,
176 window_padding,
177 content_offset_y: 0.0,
178 content_offset_x: 0.0,
179 preserve_aspect_ratio,
180 })
181 }
182
183 fn create_pipeline(
185 device: &Device,
186 format: TextureFormat,
187 bind_group_layout: &BindGroupLayout,
188 ) -> Result<RenderPipeline, RenderError> {
189 let shader = device.create_shader_module(ShaderModuleDescriptor {
190 label: Some("Sixel Shader"),
191 source: ShaderSource::Wgsl(include_str!("shaders/sixel.wgsl").into()),
192 });
193
194 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
195 label: Some("Sixel Pipeline Layout"),
196 bind_group_layouts: &[Some(bind_group_layout)],
197 immediate_size: 0,
198 });
199
200 Ok(device.create_render_pipeline(&RenderPipelineDescriptor {
201 label: Some("Sixel Pipeline"),
202 layout: Some(&pipeline_layout),
203 vertex: VertexState {
204 module: &shader,
205 entry_point: Some("vs_main"),
206 buffers: &[VertexBufferLayout {
207 array_stride: std::mem::size_of::<SixelInstance>() as u64,
208 step_mode: VertexStepMode::Instance,
209 attributes: &vertex_attr_array![
210 0 => Float32x2, 1 => Float32x4, 2 => Float32x2, 3 => Float32, ],
215 }],
216 compilation_options: Default::default(),
217 },
218 fragment: Some(FragmentState {
219 module: &shader,
220 entry_point: Some("fs_main"),
221 targets: &[Some(ColorTargetState {
222 format,
223 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
225 write_mask: ColorWrites::ALL,
226 })],
227 compilation_options: Default::default(),
228 }),
229 primitive: PrimitiveState {
230 topology: PrimitiveTopology::TriangleStrip,
231 ..Default::default()
232 },
233 depth_stencil: None,
234 multisample: MultisampleState::default(),
235 cache: None,
236 multiview_mask: None,
237 }))
238 }
239
240 pub fn get_or_create_texture(
250 &mut self,
251 device: &Device,
252 queue: &Queue,
253 id: u64,
254 rgba_data: &[u8],
255 width: u32,
256 height: u32,
257 ) -> Result<(), RenderError> {
258 if let Some(cached) = self.texture_cache.get_mut(&id) {
261 cached.last_used = Instant::now();
263
264 const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
272 if id & VIRTUAL_PLACEMENT_ID_FLAG != 0 {
273 return Ok(());
274 }
275
276 let expected_size = (width * height * 4) as usize;
279 if rgba_data.len() != expected_size {
280 return Err(RenderError::InvalidTextureData {
281 expected: expected_size,
282 actual: rgba_data.len(),
283 });
284 }
285
286 queue.write_texture(
288 TexelCopyTextureInfo {
289 texture: &cached.texture.texture,
290 mip_level: 0,
291 origin: Origin3d::ZERO,
292 aspect: TextureAspect::All,
293 },
294 rgba_data,
295 TexelCopyBufferLayout {
296 offset: 0,
297 bytes_per_row: Some(4 * width),
298 rows_per_image: Some(height),
299 },
300 Extent3d {
301 width,
302 height,
303 depth_or_array_layers: 1,
304 },
305 );
306
307 return Ok(());
308 }
309
310 let expected_size = (width * height * 4) as usize;
312 if rgba_data.len() != expected_size {
313 return Err(RenderError::InvalidTextureData {
314 expected: expected_size,
315 actual: rgba_data.len(),
316 });
317 }
318
319 if self.texture_cache.len() >= MAX_TEXTURE_CACHE_SIZE
321 && let Some((&lru_id, _)) = self
322 .texture_cache
323 .iter()
324 .min_by_key(|(_, cached)| cached.last_used)
325 {
326 log::debug!(
327 "[GRAPHICS] Evicting LRU texture: id={}, cache_size={}",
328 lru_id,
329 self.texture_cache.len()
330 );
331 self.texture_cache.remove(&lru_id);
332 }
333
334 let texture = device.create_texture(&TextureDescriptor {
336 label: Some(&format!("Sixel Texture {}", id)),
337 size: Extent3d {
338 width,
339 height,
340 depth_or_array_layers: 1,
341 },
342 mip_level_count: 1,
343 sample_count: 1,
344 dimension: TextureDimension::D2,
345 format: TextureFormat::Rgba8Unorm,
346 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
347 view_formats: &[],
348 });
349
350 queue.write_texture(
352 TexelCopyTextureInfo {
353 texture: &texture,
354 mip_level: 0,
355 origin: Origin3d::ZERO,
356 aspect: TextureAspect::All,
357 },
358 rgba_data,
359 TexelCopyBufferLayout {
360 offset: 0,
361 bytes_per_row: Some(4 * width),
362 rows_per_image: Some(height),
363 },
364 Extent3d {
365 width,
366 height,
367 depth_or_array_layers: 1,
368 },
369 );
370
371 let view = texture.create_view(&TextureViewDescriptor::default());
372
373 let bind_group = device.create_bind_group(&BindGroupDescriptor {
375 label: Some(&format!("Sixel Bind Group {}", id)),
376 layout: &self.bind_group_layout,
377 entries: &[
378 BindGroupEntry {
379 binding: 0,
380 resource: BindingResource::TextureView(&view),
381 },
382 BindGroupEntry {
383 binding: 1,
384 resource: BindingResource::Sampler(&self.sampler),
385 },
386 ],
387 });
388
389 self.texture_cache.insert(
391 id,
392 CachedTexture {
393 texture: SixelTextureInfo {
394 texture,
395 view,
396 bind_group,
397 width,
398 height,
399 },
400 last_used: Instant::now(),
401 },
402 );
403
404 log::debug!(
405 "[GRAPHICS] Created sixel texture: id={}, size={}x{}, cache_size={}/{}",
406 id,
407 width,
408 height,
409 self.texture_cache.len(),
410 MAX_TEXTURE_CACHE_SIZE
411 );
412
413 Ok(())
414 }
415
416 pub fn render(
426 &mut self,
427 device: &Device,
428 queue: &Queue,
429 render_pass: &mut RenderPass,
430 graphics: &[GraphicRenderInfo],
431 window_width: f32,
432 window_height: f32,
433 ) -> Result<(), RenderError> {
434 if graphics.is_empty() {
435 return Ok(());
436 }
437
438 let mut instances = Vec::with_capacity(graphics.len());
440 for g in graphics {
441 let (id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) = (
442 g.id,
443 g.screen_row,
444 g.col,
445 g.width_cells,
446 g.height_cells,
447 g.alpha,
448 g.scroll_offset_rows,
449 );
450 if let Some(cached) = self.texture_cache.get_mut(&id) {
452 cached.last_used = Instant::now();
453 let tex_info = &cached.texture;
454
455 let adjusted_row = row + scroll_offset_rows as isize;
460 let x =
461 (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
462 / window_width;
463 let y = (self.window_padding
464 + self.content_offset_y
465 + adjusted_row as f32 * self.cell_height)
466 / window_height;
467
468 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
472 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
473 (pixels_scrolled / tex_info.height as f32).min(0.99)
474 } else {
475 0.0
476 };
477 let tex_v_height = 1.0 - tex_v_start;
478
479 const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
498 let is_virtual_placement = id & VIRTUAL_PLACEMENT_ID_FLAG != 0;
499 let (width, height) = if self.preserve_aspect_ratio && !is_virtual_placement {
500 let visible_height_pixels = if scroll_offset_rows > 0 {
503 (tex_info.height as f32 * tex_v_height).max(1.0)
504 } else {
505 tex_info.height as f32
506 };
507 (
508 tex_info.width as f32 / window_width,
509 visible_height_pixels / window_height,
510 )
511 } else {
512 let cell_w = _width_cells as f32 * self.cell_width / window_width;
514 let visible_cell_rows = if scroll_offset_rows > 0 {
515 (_height_cells as f32 * tex_v_height).max(0.0)
516 } else {
517 _height_cells as f32
518 };
519 let cell_h = visible_cell_rows * self.cell_height / window_height;
520 (cell_w, cell_h)
521 };
522
523 instances.push(SixelInstance {
524 position: [x, y],
525 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height], size: [width, height],
527 alpha,
528 _padding: 0.0,
529 });
530 }
531 }
532
533 if instances.is_empty() {
534 return Ok(());
535 }
536
537 log::debug!(
539 "[GRAPHICS] Rendering {} sixel graphics (from {} total graphics provided)",
540 instances.len(),
541 graphics.len()
542 );
543
544 let required_capacity = instances.len();
546 if required_capacity > self.instance_capacity {
547 let new_capacity = (required_capacity * 2).max(32);
548 self.instance_buffer = device.create_buffer(&BufferDescriptor {
549 label: Some("Sixel Instance Buffer"),
550 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
551 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
552 mapped_at_creation: false,
553 });
554 self.instance_capacity = new_capacity;
555 }
556
557 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
559
560 render_pass.set_pipeline(&self.pipeline);
562
563 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
565
566 let mut instance_idx = 0u32;
568 for g in graphics {
569 if let Some(cached) = self.texture_cache.get(&g.id) {
570 render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
571 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
572 instance_idx += 1;
573 }
574 }
575
576 Ok(())
577 }
578
579 pub fn render_for_pane(
595 &mut self,
596 device: &Device,
597 queue: &Queue,
598 render_pass: &mut RenderPass,
599 graphics: &[GraphicRenderInfo],
600 pane_geometry: PaneRenderGeometry,
601 ) -> Result<(), RenderError> {
602 let PaneRenderGeometry {
603 window_width,
604 window_height,
605 pane_origin_x,
606 pane_origin_y,
607 } = pane_geometry;
608 if graphics.is_empty() {
609 return Ok(());
610 }
611
612 let mut instances = Vec::with_capacity(graphics.len());
614 for g in graphics {
615 let (id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) = (
616 g.id,
617 g.screen_row,
618 g.col,
619 g.width_cells,
620 g.height_cells,
621 g.alpha,
622 g.scroll_offset_rows,
623 );
624 if let Some(cached) = self.texture_cache.get_mut(&id) {
626 cached.last_used = Instant::now();
627 let tex_info = &cached.texture;
628
629 let adjusted_row = row + scroll_offset_rows as isize;
631 let x = (pane_origin_x + col as f32 * self.cell_width) / window_width;
632 let y = (pane_origin_y + adjusted_row as f32 * self.cell_height) / window_height;
633
634 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
636 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
637 (pixels_scrolled / tex_info.height as f32).min(0.99)
638 } else {
639 0.0
640 };
641 let tex_v_height = 1.0 - tex_v_start;
642
643 const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
648 let is_virtual_placement = id & VIRTUAL_PLACEMENT_ID_FLAG != 0;
649 let (width, height) = if self.preserve_aspect_ratio && !is_virtual_placement {
650 let visible_height_pixels = if scroll_offset_rows > 0 {
651 (tex_info.height as f32 * tex_v_height).max(1.0)
652 } else {
653 tex_info.height as f32
654 };
655 (
656 tex_info.width as f32 / window_width,
657 visible_height_pixels / window_height,
658 )
659 } else {
660 let cell_w = _width_cells as f32 * self.cell_width / window_width;
661 let visible_cell_rows = if scroll_offset_rows > 0 {
662 (_height_cells as f32 * tex_v_height).max(0.0)
663 } else {
664 _height_cells as f32
665 };
666 let cell_h = visible_cell_rows * self.cell_height / window_height;
667 (cell_w, cell_h)
668 };
669
670 instances.push(SixelInstance {
671 position: [x, y],
672 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height],
673 size: [width, height],
674 alpha,
675 _padding: 0.0,
676 });
677 }
678 }
679
680 if instances.is_empty() {
681 return Ok(());
682 }
683
684 let required_capacity = instances.len();
686 if required_capacity > self.instance_capacity {
687 let new_capacity = (required_capacity * 2).max(32);
688 self.instance_buffer = device.create_buffer(&BufferDescriptor {
689 label: Some("Sixel Instance Buffer"),
690 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
691 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
692 mapped_at_creation: false,
693 });
694 self.instance_capacity = new_capacity;
695 }
696
697 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
699
700 render_pass.set_pipeline(&self.pipeline);
702 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
703
704 let mut instance_idx = 0u32;
705 for g in graphics {
706 if let Some(cached) = self.texture_cache.get(&g.id) {
707 render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
708 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
709 instance_idx += 1;
710 }
711 }
712
713 Ok(())
714 }
715
716 pub fn remove_texture(&mut self, id: u64) {
718 self.texture_cache.remove(&id);
719 }
720
721 pub fn clear_cache(&mut self) {
723 self.texture_cache.clear();
724 }
725
726 pub fn cache_size(&self) -> usize {
728 self.texture_cache.len()
729 }
730
731 pub fn update_cell_dimensions(
733 &mut self,
734 cell_width: f32,
735 cell_height: f32,
736 window_padding: f32,
737 ) {
738 self.cell_width = cell_width;
739 self.cell_height = cell_height;
740 self.window_padding = window_padding;
741 }
742
743 pub fn set_content_offset_y(&mut self, offset: f32) {
745 self.content_offset_y = offset;
746 }
747
748 pub fn set_content_offset_x(&mut self, offset: f32) {
750 self.content_offset_x = offset;
751 }
752
753 pub fn set_preserve_aspect_ratio(&mut self, preserve: bool) {
755 self.preserve_aspect_ratio = preserve;
756 }
757
758 pub fn update_scaling_mode(&mut self, device: &Device, scaling_mode: ImageScalingMode) {
763 self.sampler = gpu_utils::create_sampler_with_filter(
764 device,
765 scaling_mode.to_filter_mode(),
766 Some("Sixel Sampler"),
767 );
768 self.texture_cache.clear();
770 }
771}