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: &[bind_group_layout],
196 push_constant_ranges: &[],
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 multiview: None,
235 cache: 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 let expected_size = (width * height * 4) as usize;
266 if rgba_data.len() != expected_size {
267 return Err(RenderError::InvalidTextureData {
268 expected: expected_size,
269 actual: rgba_data.len(),
270 });
271 }
272
273 queue.write_texture(
275 TexelCopyTextureInfo {
276 texture: &cached.texture.texture,
277 mip_level: 0,
278 origin: Origin3d::ZERO,
279 aspect: TextureAspect::All,
280 },
281 rgba_data,
282 TexelCopyBufferLayout {
283 offset: 0,
284 bytes_per_row: Some(4 * width),
285 rows_per_image: Some(height),
286 },
287 Extent3d {
288 width,
289 height,
290 depth_or_array_layers: 1,
291 },
292 );
293
294 return Ok(());
295 }
296
297 let expected_size = (width * height * 4) as usize;
299 if rgba_data.len() != expected_size {
300 return Err(RenderError::InvalidTextureData {
301 expected: expected_size,
302 actual: rgba_data.len(),
303 });
304 }
305
306 if self.texture_cache.len() >= MAX_TEXTURE_CACHE_SIZE
308 && let Some((&lru_id, _)) = self
309 .texture_cache
310 .iter()
311 .min_by_key(|(_, cached)| cached.last_used)
312 {
313 log::debug!(
314 "[GRAPHICS] Evicting LRU texture: id={}, cache_size={}",
315 lru_id,
316 self.texture_cache.len()
317 );
318 self.texture_cache.remove(&lru_id);
319 }
320
321 let texture = device.create_texture(&TextureDescriptor {
323 label: Some(&format!("Sixel Texture {}", id)),
324 size: Extent3d {
325 width,
326 height,
327 depth_or_array_layers: 1,
328 },
329 mip_level_count: 1,
330 sample_count: 1,
331 dimension: TextureDimension::D2,
332 format: TextureFormat::Rgba8Unorm,
333 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
334 view_formats: &[],
335 });
336
337 queue.write_texture(
339 TexelCopyTextureInfo {
340 texture: &texture,
341 mip_level: 0,
342 origin: Origin3d::ZERO,
343 aspect: TextureAspect::All,
344 },
345 rgba_data,
346 TexelCopyBufferLayout {
347 offset: 0,
348 bytes_per_row: Some(4 * width),
349 rows_per_image: Some(height),
350 },
351 Extent3d {
352 width,
353 height,
354 depth_or_array_layers: 1,
355 },
356 );
357
358 let view = texture.create_view(&TextureViewDescriptor::default());
359
360 let bind_group = device.create_bind_group(&BindGroupDescriptor {
362 label: Some(&format!("Sixel Bind Group {}", id)),
363 layout: &self.bind_group_layout,
364 entries: &[
365 BindGroupEntry {
366 binding: 0,
367 resource: BindingResource::TextureView(&view),
368 },
369 BindGroupEntry {
370 binding: 1,
371 resource: BindingResource::Sampler(&self.sampler),
372 },
373 ],
374 });
375
376 self.texture_cache.insert(
378 id,
379 CachedTexture {
380 texture: SixelTextureInfo {
381 texture,
382 view,
383 bind_group,
384 width,
385 height,
386 },
387 last_used: Instant::now(),
388 },
389 );
390
391 log::debug!(
392 "[GRAPHICS] Created sixel texture: id={}, size={}x{}, cache_size={}/{}",
393 id,
394 width,
395 height,
396 self.texture_cache.len(),
397 MAX_TEXTURE_CACHE_SIZE
398 );
399
400 Ok(())
401 }
402
403 pub fn render(
413 &mut self,
414 device: &Device,
415 queue: &Queue,
416 render_pass: &mut RenderPass,
417 graphics: &[GraphicRenderInfo],
418 window_width: f32,
419 window_height: f32,
420 ) -> Result<(), RenderError> {
421 if graphics.is_empty() {
422 return Ok(());
423 }
424
425 let mut instances = Vec::with_capacity(graphics.len());
427 for g in graphics {
428 let (id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) = (
429 g.id,
430 g.screen_row,
431 g.col,
432 g.width_cells,
433 g.height_cells,
434 g.alpha,
435 g.scroll_offset_rows,
436 );
437 if let Some(cached) = self.texture_cache.get_mut(&id) {
439 cached.last_used = Instant::now();
440 let tex_info = &cached.texture;
441
442 let adjusted_row = row + scroll_offset_rows as isize;
447 let x =
448 (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
449 / window_width;
450 let y = (self.window_padding
451 + self.content_offset_y
452 + adjusted_row as f32 * self.cell_height)
453 / window_height;
454
455 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
459 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
460 (pixels_scrolled / tex_info.height as f32).min(0.99)
461 } else {
462 0.0
463 };
464 let tex_v_height = 1.0 - tex_v_start;
465
466 let (width, height) = if self.preserve_aspect_ratio {
468 let visible_height_pixels = if scroll_offset_rows > 0 {
471 (tex_info.height as f32 * tex_v_height).max(1.0)
472 } else {
473 tex_info.height as f32
474 };
475 (
476 tex_info.width as f32 / window_width,
477 visible_height_pixels / window_height,
478 )
479 } else {
480 let cell_w = _width_cells as f32 * self.cell_width / window_width;
482 let visible_cell_rows = if scroll_offset_rows > 0 {
483 (_height_cells as f32 * tex_v_height).max(0.0)
484 } else {
485 _height_cells as f32
486 };
487 let cell_h = visible_cell_rows * self.cell_height / window_height;
488 (cell_w, cell_h)
489 };
490
491 instances.push(SixelInstance {
492 position: [x, y],
493 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height], size: [width, height],
495 alpha,
496 _padding: 0.0,
497 });
498 }
499 }
500
501 if instances.is_empty() {
502 return Ok(());
503 }
504
505 log::debug!(
507 "[GRAPHICS] Rendering {} sixel graphics (from {} total graphics provided)",
508 instances.len(),
509 graphics.len()
510 );
511
512 let required_capacity = instances.len();
514 if required_capacity > self.instance_capacity {
515 let new_capacity = (required_capacity * 2).max(32);
516 self.instance_buffer = device.create_buffer(&BufferDescriptor {
517 label: Some("Sixel Instance Buffer"),
518 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
519 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
520 mapped_at_creation: false,
521 });
522 self.instance_capacity = new_capacity;
523 }
524
525 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
527
528 render_pass.set_pipeline(&self.pipeline);
530
531 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
533
534 let mut instance_idx = 0u32;
536 for g in graphics {
537 if let Some(cached) = self.texture_cache.get(&g.id) {
538 render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
539 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
540 instance_idx += 1;
541 }
542 }
543
544 Ok(())
545 }
546
547 pub fn render_for_pane(
563 &mut self,
564 device: &Device,
565 queue: &Queue,
566 render_pass: &mut RenderPass,
567 graphics: &[GraphicRenderInfo],
568 pane_geometry: PaneRenderGeometry,
569 ) -> Result<(), RenderError> {
570 let PaneRenderGeometry {
571 window_width,
572 window_height,
573 pane_origin_x,
574 pane_origin_y,
575 } = pane_geometry;
576 if graphics.is_empty() {
577 return Ok(());
578 }
579
580 let mut instances = Vec::with_capacity(graphics.len());
582 for g in graphics {
583 let (id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) = (
584 g.id,
585 g.screen_row,
586 g.col,
587 g.width_cells,
588 g.height_cells,
589 g.alpha,
590 g.scroll_offset_rows,
591 );
592 if let Some(cached) = self.texture_cache.get_mut(&id) {
594 cached.last_used = Instant::now();
595 let tex_info = &cached.texture;
596
597 let adjusted_row = row + scroll_offset_rows as isize;
599 let x = (pane_origin_x + col as f32 * self.cell_width) / window_width;
600 let y = (pane_origin_y + adjusted_row as f32 * self.cell_height) / window_height;
601
602 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
604 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
605 (pixels_scrolled / tex_info.height as f32).min(0.99)
606 } else {
607 0.0
608 };
609 let tex_v_height = 1.0 - tex_v_start;
610
611 let (width, height) = if self.preserve_aspect_ratio {
613 let visible_height_pixels = if scroll_offset_rows > 0 {
614 (tex_info.height as f32 * tex_v_height).max(1.0)
615 } else {
616 tex_info.height as f32
617 };
618 (
619 tex_info.width as f32 / window_width,
620 visible_height_pixels / window_height,
621 )
622 } else {
623 let cell_w = _width_cells as f32 * self.cell_width / window_width;
624 let visible_cell_rows = if scroll_offset_rows > 0 {
625 (_height_cells as f32 * tex_v_height).max(0.0)
626 } else {
627 _height_cells as f32
628 };
629 let cell_h = visible_cell_rows * self.cell_height / window_height;
630 (cell_w, cell_h)
631 };
632
633 instances.push(SixelInstance {
634 position: [x, y],
635 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height],
636 size: [width, height],
637 alpha,
638 _padding: 0.0,
639 });
640 }
641 }
642
643 if instances.is_empty() {
644 return Ok(());
645 }
646
647 let required_capacity = instances.len();
649 if required_capacity > self.instance_capacity {
650 let new_capacity = (required_capacity * 2).max(32);
651 self.instance_buffer = device.create_buffer(&BufferDescriptor {
652 label: Some("Sixel Instance Buffer"),
653 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
654 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
655 mapped_at_creation: false,
656 });
657 self.instance_capacity = new_capacity;
658 }
659
660 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
662
663 render_pass.set_pipeline(&self.pipeline);
665 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
666
667 let mut instance_idx = 0u32;
668 for g in graphics {
669 if let Some(cached) = self.texture_cache.get(&g.id) {
670 render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
671 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
672 instance_idx += 1;
673 }
674 }
675
676 Ok(())
677 }
678
679 pub fn remove_texture(&mut self, id: u64) {
681 self.texture_cache.remove(&id);
682 }
683
684 pub fn clear_cache(&mut self) {
686 self.texture_cache.clear();
687 }
688
689 pub fn cache_size(&self) -> usize {
691 self.texture_cache.len()
692 }
693
694 pub fn update_cell_dimensions(
696 &mut self,
697 cell_width: f32,
698 cell_height: f32,
699 window_padding: f32,
700 ) {
701 self.cell_width = cell_width;
702 self.cell_height = cell_height;
703 self.window_padding = window_padding;
704 }
705
706 pub fn set_content_offset_y(&mut self, offset: f32) {
708 self.content_offset_y = offset;
709 }
710
711 pub fn set_content_offset_x(&mut self, offset: f32) {
713 self.content_offset_x = offset;
714 }
715
716 pub fn set_preserve_aspect_ratio(&mut self, preserve: bool) {
718 self.preserve_aspect_ratio = preserve;
719 }
720
721 pub fn update_scaling_mode(&mut self, device: &Device, scaling_mode: ImageScalingMode) {
726 self.sampler = gpu_utils::create_sampler_with_filter(
727 device,
728 scaling_mode.to_filter_mode(),
729 Some("Sixel Sampler"),
730 );
731 self.texture_cache.clear();
733 }
734}