1use super::Renderer;
2use crate::cell_renderer::Cell;
3use crate::graphics_renderer::GraphicRenderInfo;
4use anyhow::Result;
5use par_term_emu_core_rust::graphics::TerminalGraphic;
6use par_term_emu_core_rust::graphics::placeholder::{PLACEHOLDER_CHAR, diacritic_to_number};
7
8const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
15
16fn virtual_placement_cache_id(image_id: u32, placement_id: u32) -> u64 {
18 VIRTUAL_PLACEMENT_ID_FLAG | ((placement_id as u64) << 32) | image_id as u64
19}
20
21fn rgba_diag_summary(pixels: &[u8]) -> String {
23 let (rgba_pixels, _) = pixels.as_chunks::<4>();
24 let alpha = rgba_pixels.iter().filter(|px| px[3] != 0).count();
25 let first = pixels
26 .get(..4)
27 .map(|px| format!("{:02x}{:02x}{:02x}{:02x}", px[0], px[1], px[2], px[3]))
28 .unwrap_or_else(|| "none".to_string());
29 format!(
30 "buf_len={}, non_zero_alpha={}, first_rgba={}",
31 pixels.len(),
32 alpha,
33 first
34 )
35}
36
37fn decode_placeholder_cell(cell: &Cell) -> Option<(u32, u32, u16, u16)> {
51 let mut chars = cell.grapheme.chars();
52 if chars.next()? != PLACEHOLDER_CHAR {
53 return None;
54 }
55 let row_idx = diacritic_to_number(chars.next()?)?;
56 let col_idx = diacritic_to_number(chars.next()?)?;
57 let msb_u8 = chars
61 .next()
62 .and_then(diacritic_to_number)
63 .map(|n| if n <= u8::MAX as u16 { n as u8 } else { 0 })
64 .unwrap_or(0);
65
66 let [r, g, b, _a] = cell.fg_color;
69 let image_id = ((msb_u8 as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32;
70 Some((image_id, 0, row_idx, col_idx))
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) struct VirtualPlacementHit {
79 pub image_id: u32,
80 pub placement_id: u32,
81 pub start_col: usize,
82 pub start_row: usize,
83 pub width_cells: usize,
84 pub height_cells: usize,
85}
86
87pub(crate) fn scan_placeholder_cells(
95 cells: &[Cell],
96 cols: usize,
97 rows: usize,
98) -> Vec<VirtualPlacementHit> {
99 use std::collections::HashMap;
100
101 let mut bboxes: HashMap<(u32, u32), (usize, usize, usize, usize)> = HashMap::new();
103
104 for row in 0..rows {
105 let row_start = row * cols;
106 if row_start >= cells.len() {
107 break;
108 }
109 let row_end = (row_start + cols).min(cells.len());
110 for (col_off, cell) in cells[row_start..row_end].iter().enumerate() {
111 let Some((image_id, placement_id, _r_idx, _c_idx)) = decode_placeholder_cell(cell)
112 else {
113 continue;
114 };
115 let col = col_off;
116 bboxes
117 .entry((image_id, placement_id))
118 .and_modify(|b| {
119 if col < b.0 {
120 b.0 = col;
121 }
122 if row < b.1 {
123 b.1 = row;
124 }
125 if col > b.2 {
126 b.2 = col;
127 }
128 if row > b.3 {
129 b.3 = row;
130 }
131 })
132 .or_insert((col, row, col, row));
133 }
134 }
135
136 let mut hits: Vec<VirtualPlacementHit> = bboxes
137 .into_iter()
138 .map(
139 |((image_id, placement_id), (min_c, min_r, max_c, max_r))| VirtualPlacementHit {
140 image_id,
141 placement_id,
142 start_col: min_c,
143 start_row: min_r,
144 width_cells: max_c - min_c + 1,
145 height_cells: max_r - min_r + 1,
146 },
147 )
148 .collect();
149 hits.sort_by_key(|h| (h.image_id, h.placement_id, h.start_row, h.start_col));
151 hits
152}
153
154impl Renderer {
155 pub fn update_graphics(
163 &mut self,
164 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
165 view_scroll_offset: usize,
166 scrollback_len: usize,
167 visible_rows: usize,
168 ) -> Result<()> {
169 let had_graphics = !self.sixel_graphics.is_empty();
171
172 self.sixel_graphics.clear();
174
175 let total_lines = scrollback_len + visible_rows;
180 let view_end = total_lines.saturating_sub(view_scroll_offset);
181 let view_start = view_end.saturating_sub(visible_rows);
182
183 for graphic in graphics {
185 let id = graphic.id;
187 let (col, row) = graphic.position;
188
189 let core_cell_height = graphic
193 .cell_dimensions
194 .map(|(_, h)| h as f32)
195 .unwrap_or(2.0)
196 .max(1.0);
197 let display_cell_height = self.cell_renderer.cell_height().max(1.0);
198 let scroll_offset_in_display_rows = (graphic.scroll_offset_rows as f32
199 * core_cell_height
200 / display_cell_height)
201 .round() as usize;
202
203 let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
205 sb_row as isize - view_start as isize
208 } else {
209 let absolute_row =
213 scrollback_len.saturating_sub(scroll_offset_in_display_rows) + row;
214
215 log::trace!(
216 "[RENDERER] CALC: scrollback_len={}, row={}, scroll_offset_rows={}, scroll_in_display_rows={}, absolute_row={}, view_start={}, screen_row={}",
217 scrollback_len,
218 row,
219 graphic.scroll_offset_rows,
220 scroll_offset_in_display_rows,
221 absolute_row,
222 view_start,
223 absolute_row as isize - view_start as isize
224 );
225
226 absolute_row as isize - view_start as isize
227 };
228
229 if log::log_enabled!(log::Level::Debug) {
230 log::debug!(
231 "[RENDERER] Graphics update: id={}, protocol={:?}, pos=({},{}), screen_row={}, scrollback_row={:?}, scroll_offset_rows={}, size={}x{}, pixels=({}), view=[{},{})",
232 id,
233 graphic.protocol,
234 col,
235 row,
236 screen_row,
237 graphic.scrollback_row,
238 graphic.scroll_offset_rows,
239 graphic.width,
240 graphic.height,
241 rgba_diag_summary(&graphic.pixels),
242 view_start,
243 view_end
244 );
245 }
246
247 self.graphics_renderer.get_or_create_texture(
249 self.cell_renderer.device(),
250 self.cell_renderer.queue(),
251 id,
252 &graphic.pixels, graphic.width as u32,
254 graphic.height as u32,
255 )?;
256
257 let (width_cells, height_cells) = graphic.cell_span(
259 self.cell_renderer.cell_width() as u32,
260 self.cell_renderer.cell_height() as u32,
261 );
262
263 let effective_clip_rows = if screen_row < 0 {
267 (-screen_row) as usize
268 } else {
269 0
270 };
271
272 self.sixel_graphics.push(GraphicRenderInfo {
273 id,
274 screen_row,
275 col,
276 width_cells,
277 height_cells,
278 alpha: 1.0,
279 scroll_offset_rows: effective_clip_rows,
280 destination_offset_x: graphic.placement.x_offset,
281 destination_offset_y: graphic.placement.y_offset,
282 source_crop: [
283 graphic.placement.source_x,
284 graphic.placement.source_y,
285 graphic.placement.source_width,
286 graphic.placement.source_height,
287 ],
288 has_cols: graphic.placement.columns.filter(|&v| v > 0).is_some(),
289 has_rows: graphic.placement.rows.filter(|&v| v > 0).is_some(),
290 });
291 }
292
293 if !graphics.is_empty() || had_graphics {
295 self.dirty = true;
296 }
297
298 Ok(())
299 }
300
301 pub fn update_pane_graphics(
308 &mut self,
309 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
310 view_scroll_offset: usize,
311 scrollback_len: usize,
312 visible_rows: usize,
313 ) -> Result<Vec<GraphicRenderInfo>> {
314 let total_lines = scrollback_len + visible_rows;
315 let view_end = total_lines.saturating_sub(view_scroll_offset);
316 let view_start = view_end.saturating_sub(visible_rows);
317
318 log::debug!(
319 "[PANE_GRAPHICS] update_pane_graphics: scrollback_len={}, visible_rows={}, view_scroll_offset={}, total_lines={}, view_start={}, view_end={}, graphics_count={}",
320 scrollback_len,
321 visible_rows,
322 view_scroll_offset,
323 total_lines,
324 view_start,
325 view_end,
326 graphics.len()
327 );
328
329 let mut positioned = Vec::new();
330
331 for graphic in graphics {
332 let id = graphic.id;
333 let (col, row) = graphic.position;
334
335 let core_cell_height = graphic
340 .cell_dimensions
341 .map(|(_, h)| h as f32)
342 .unwrap_or(2.0)
343 .max(1.0);
344 let display_cell_height = self.cell_renderer.cell_height().max(1.0);
345 let scroll_offset_in_display_rows = (graphic.scroll_offset_rows as f32
346 * core_cell_height
347 / display_cell_height)
348 .round() as usize;
349
350 let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
351 let sr = sb_row as isize - view_start as isize;
352 log::debug!(
353 "[PANE_GRAPHICS] scrollback graphic id={}: sb_row={}, view_start={}, screen_row={}",
354 id,
355 sb_row,
356 view_start,
357 sr
358 );
359 sr
360 } else {
361 let absolute_row =
362 scrollback_len.saturating_sub(scroll_offset_in_display_rows) + row;
363 let sr = absolute_row as isize - view_start as isize;
364 log::debug!(
365 "[PANE_GRAPHICS] current graphic id={}: scrollback_len={}, scroll_offset_rows={}, core_cell_h={}, disp_cell_h={}, scroll_in_display_rows={}, row={}, absolute_row={}, view_start={}, screen_row={}",
366 id,
367 scrollback_len,
368 graphic.scroll_offset_rows,
369 core_cell_height,
370 display_cell_height,
371 scroll_offset_in_display_rows,
372 row,
373 absolute_row,
374 view_start,
375 sr
376 );
377 sr
378 };
379
380 if log::log_enabled!(log::Level::Debug) {
381 log::debug!(
382 "[PANE_GRAPHICS] texture upload: id={}, protocol={:?}, size={}x{}, {}",
383 id,
384 graphic.protocol,
385 graphic.width,
386 graphic.height,
387 rgba_diag_summary(&graphic.pixels)
388 );
389 }
390 self.graphics_renderer.get_or_create_texture(
392 self.cell_renderer.device(),
393 self.cell_renderer.queue(),
394 id,
395 &graphic.pixels,
396 graphic.width as u32,
397 graphic.height as u32,
398 )?;
399
400 let (width_cells, height_cells) = graphic.cell_span(
401 self.cell_renderer.cell_width() as u32,
402 self.cell_renderer.cell_height() as u32,
403 );
404
405 let effective_clip_rows = if screen_row < 0 {
406 (-screen_row) as usize
407 } else {
408 0
409 };
410
411 positioned.push(GraphicRenderInfo {
412 id,
413 screen_row,
414 col,
415 width_cells,
416 height_cells,
417 alpha: 1.0,
418 scroll_offset_rows: effective_clip_rows,
419 destination_offset_x: graphic.placement.x_offset,
420 destination_offset_y: graphic.placement.y_offset,
421 source_crop: [
422 graphic.placement.source_x,
423 graphic.placement.source_y,
424 graphic.placement.source_width,
425 graphic.placement.source_height,
426 ],
427 has_cols: graphic.placement.columns.filter(|&v| v > 0).is_some(),
428 has_rows: graphic.placement.rows.filter(|&v| v > 0).is_some(),
429 });
430 }
431
432 Ok(positioned)
433 }
434
435 pub(crate) fn update_pane_virtual_placements(
443 &mut self,
444 cells: &[Cell],
445 cols: usize,
446 rows: usize,
447 virtual_placements: &[TerminalGraphic],
448 ) -> Result<Vec<GraphicRenderInfo>> {
449 let hits = scan_placeholder_cells(cells, cols, rows);
450 if hits.is_empty() {
451 return Ok(Vec::new());
452 }
453
454 let mut out = Vec::with_capacity(hits.len());
455 for hit in hits {
456 let graphic = virtual_placements
460 .iter()
461 .find(|g| {
462 g.kitty_image_id == Some(hit.image_id)
463 && g.kitty_placement_id.unwrap_or(0) == hit.placement_id
464 })
465 .or_else(|| {
466 if hit.placement_id == 0 {
467 virtual_placements
468 .iter()
469 .find(|g| g.kitty_image_id == Some(hit.image_id))
470 } else {
471 None
472 }
473 });
474 let Some(graphic) = graphic else {
475 log::trace!(
476 "[VPLACE] no virtual placement for image_id={}, placement_id={}",
477 hit.image_id,
478 hit.placement_id
479 );
480 continue;
481 };
482
483 let cache_id = virtual_placement_cache_id(hit.image_id, hit.placement_id);
484 self.graphics_renderer.get_or_create_texture(
485 self.cell_renderer.device(),
486 self.cell_renderer.queue(),
487 cache_id,
488 &graphic.pixels,
489 graphic.width as u32,
490 graphic.height as u32,
491 )?;
492
493 out.push(GraphicRenderInfo {
494 id: cache_id,
495 screen_row: hit.start_row as isize,
496 col: hit.start_col,
497 width_cells: hit.width_cells,
498 height_cells: hit.height_cells,
499 alpha: 1.0,
500 scroll_offset_rows: 0,
501 destination_offset_x: 0,
502 destination_offset_y: 0,
503 source_crop: [0; 4],
504 has_cols: true,
505 has_rows: true,
506 });
507 }
508 Ok(out)
509 }
510
511 #[allow(clippy::too_many_arguments)] pub(crate) fn render_pane_sixel_graphics(
518 &mut self,
519 surface_view: &wgpu::TextureView,
520 viewport: &crate::cell_renderer::PaneViewport,
521 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
522 scroll_offset: usize,
523 scrollback_len: usize,
524 visible_rows: usize,
525 cells: &[Cell],
526 cols: usize,
527 virtual_placements: &[TerminalGraphic],
528 ) -> Result<()> {
529 let mut positioned =
530 self.update_pane_graphics(graphics, scroll_offset, scrollback_len, visible_rows)?;
531
532 if !virtual_placements.is_empty() && !cells.is_empty() && cols > 0 {
537 positioned.extend(self.update_pane_virtual_placements(
538 cells,
539 cols,
540 visible_rows,
541 virtual_placements,
542 )?);
543 }
544
545 if positioned.is_empty() {
546 return Ok(());
547 }
548
549 let mut encoder =
550 self.cell_renderer
551 .device()
552 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
553 label: Some("pane sixel encoder"),
554 });
555
556 {
557 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
558 label: Some("pane sixel render pass"),
559 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
560 view: surface_view,
561 resolve_target: None,
562 ops: wgpu::Operations {
563 load: wgpu::LoadOp::Load,
564 store: wgpu::StoreOp::Store,
565 },
566 depth_slice: None,
567 })],
568 depth_stencil_attachment: None,
569 timestamp_writes: None,
570 occlusion_query_set: None,
571 multiview_mask: None,
572 });
573
574 let (sx, sy, sw, sh) = viewport.to_scissor_rect();
576 render_pass.set_scissor_rect(sx, sy, sw, sh);
577
578 let (ox, oy) = viewport.content_origin();
579
580 log::debug!(
581 "[PANE_GRAPHICS] render_pane_sixel_graphics: scissor=({},{},{},{}), origin=({},{}), window={}x{}, positioned_count={}",
582 sx,
583 sy,
584 sw,
585 sh,
586 ox,
587 oy,
588 self.size.width,
589 self.size.height,
590 positioned.len()
591 );
592 for g in &positioned {
593 log::debug!(
594 "[PANE_GRAPHICS] positioned: id={}, screen_row={}, col={}, width_cells={}, height_cells={}, clip_rows={}",
595 g.id,
596 g.screen_row,
597 g.col,
598 g.width_cells,
599 g.height_cells,
600 g.scroll_offset_rows
601 );
602 }
603
604 self.graphics_renderer.render_for_pane(
605 self.cell_renderer.device(),
606 self.cell_renderer.queue(),
607 &mut render_pass,
608 &positioned,
609 crate::graphics_renderer::PaneRenderGeometry {
610 window_width: self.size.width as f32,
611 window_height: self.size.height as f32,
612 pane_origin_x: ox,
613 pane_origin_y: oy,
614 },
615 )?;
616 }
617
618 self.cell_renderer
619 .queue()
620 .submit(std::iter::once(encoder.finish()));
621
622 Ok(())
623 }
624
625 pub fn clear_sixel_cache(&mut self) {
627 self.graphics_renderer.clear_cache();
628 self.sixel_graphics.clear();
629 self.dirty = true;
630 }
631
632 pub fn sixel_cache_size(&self) -> usize {
634 self.graphics_renderer.cache_size()
635 }
636
637 pub fn remove_sixel_texture(&mut self, id: u64) {
639 self.graphics_renderer.remove_texture(id);
640 self.sixel_graphics.retain(|g| g.id != id);
641 self.dirty = true;
642 }
643}
644
645#[cfg(test)]
646mod virtual_placement_tests {
647 use super::{
655 VIRTUAL_PLACEMENT_ID_FLAG, decode_placeholder_cell, scan_placeholder_cells,
656 virtual_placement_cache_id,
657 };
658 use crate::cell_renderer::Cell;
659 use par_term_emu_core_rust::graphics::placeholder::{
660 PLACEHOLDER_CHAR, create_placeholder_with_diacritics,
661 };
662
663 fn placeholder_cell(image_id: u32, row_idx: u16, col_idx: u16) -> Cell {
666 let r = ((image_id >> 16) & 0xFF) as u8;
667 let g = ((image_id >> 8) & 0xFF) as u8;
668 let b = (image_id & 0xFF) as u8;
669 Cell {
670 grapheme: create_placeholder_with_diacritics(row_idx, col_idx, None),
671 fg_color: [r, g, b, 255],
672 ..Default::default()
673 }
674 }
675
676 fn blank_cell() -> Cell {
677 Cell {
678 grapheme: " ".to_string(),
679 ..Default::default()
680 }
681 }
682
683 fn make_grid(cells: Vec<Cell>, cols: usize) -> (Vec<Cell>, usize, usize) {
684 let rows = cells.len() / cols;
685 (cells, cols, rows)
686 }
687
688 #[test]
689 fn decode_placeholder_recovers_image_id_and_indices() {
690 let cell = placeholder_cell(0x123456, 3, 7);
691 let (image_id, placement_id, row, col) = decode_placeholder_cell(&cell).unwrap();
692 assert_eq!(image_id, 0x123456);
693 assert_eq!(placement_id, 0);
694 assert_eq!(row, 3);
695 assert_eq!(col, 7);
696 }
697
698 #[test]
699 fn decode_placeholder_rejects_non_placeholder_cells() {
700 let cell = blank_cell();
701 assert!(decode_placeholder_cell(&cell).is_none());
702
703 let mut letter = blank_cell();
704 letter.grapheme = "a".to_string();
705 assert!(decode_placeholder_cell(&letter).is_none());
706 }
707
708 #[test]
709 fn scan_finds_single_rectangle_for_single_image() {
710 let mut cells = vec![blank_cell(); 4 * 3];
716 for r in 0..2 {
717 for c in 1..4 {
718 cells[r * 4 + c] = placeholder_cell(42, r as u16, (c - 1) as u16);
719 }
720 }
721 let (cells, cols, rows) = make_grid(cells, 4);
722
723 let hits = scan_placeholder_cells(&cells, cols, rows);
724 assert_eq!(hits.len(), 1);
725 let h = hits[0];
726 assert_eq!(h.image_id, 42);
727 assert_eq!(h.placement_id, 0);
728 assert_eq!(h.start_col, 1);
729 assert_eq!(h.start_row, 0);
730 assert_eq!(h.width_cells, 3);
731 assert_eq!(h.height_cells, 2);
732 }
733
734 #[test]
735 fn scan_groups_two_adjacent_images_separately() {
736 let mut cells = Vec::with_capacity(6);
738 for c in 0..3 {
739 cells.push(placeholder_cell(7, 0, c as u16));
740 }
741 for c in 0..3 {
742 cells.push(placeholder_cell(99, 0, c as u16));
743 }
744 let (cells, cols, rows) = make_grid(cells, 6);
745
746 let hits = scan_placeholder_cells(&cells, cols, rows);
747 assert_eq!(hits.len(), 2);
748
749 let h7 = hits.iter().find(|h| h.image_id == 7).unwrap();
750 assert_eq!(h7.start_col, 0);
751 assert_eq!(h7.width_cells, 3);
752 assert_eq!(h7.height_cells, 1);
753
754 let h99 = hits.iter().find(|h| h.image_id == 99).unwrap();
755 assert_eq!(h99.start_col, 3);
756 assert_eq!(h99.width_cells, 3);
757 assert_eq!(h99.height_cells, 1);
758 }
759
760 #[test]
761 fn scan_ignores_non_placeholder_cells() {
762 let cells = vec![blank_cell(); 6];
769 let hits = scan_placeholder_cells(&cells, 6, 1);
770 assert!(hits.is_empty());
771 }
772
773 #[test]
774 fn glyph_path_recognizes_placeholder_char() {
775 let cell = placeholder_cell(1, 0, 0);
780 let first = cell.grapheme.chars().next().unwrap();
781 assert_eq!(first, '\u{10EEEE}');
782 assert_eq!(first, PLACEHOLDER_CHAR);
783 }
784
785 #[test]
786 fn cache_id_is_disjoint_from_normal_graphic_ids() {
787 let id_a = virtual_placement_cache_id(42, 0);
791 let id_b = virtual_placement_cache_id(42, 1);
792 assert_ne!(id_a, id_b);
793 assert!(id_a & VIRTUAL_PLACEMENT_ID_FLAG != 0);
794 assert!(id_b & VIRTUAL_PLACEMENT_ID_FLAG != 0);
795 }
796}