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