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 decode_placeholder_cell(cell: &Cell) -> Option<(u32, u32, u16, u16)> {
35 let mut chars = cell.grapheme.chars();
36 if chars.next()? != PLACEHOLDER_CHAR {
37 return None;
38 }
39 let row_idx = diacritic_to_number(chars.next()?)?;
40 let col_idx = diacritic_to_number(chars.next()?)?;
41 let msb_u8 = chars
45 .next()
46 .and_then(diacritic_to_number)
47 .map(|n| if n <= u8::MAX as u16 { n as u8 } else { 0 })
48 .unwrap_or(0);
49
50 let [r, g, b, _a] = cell.fg_color;
53 let image_id = ((msb_u8 as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32;
54 Some((image_id, 0, row_idx, col_idx))
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(crate) struct VirtualPlacementHit {
63 pub image_id: u32,
64 pub placement_id: u32,
65 pub start_col: usize,
66 pub start_row: usize,
67 pub width_cells: usize,
68 pub height_cells: usize,
69}
70
71pub(crate) fn scan_placeholder_cells(
79 cells: &[Cell],
80 cols: usize,
81 rows: usize,
82) -> Vec<VirtualPlacementHit> {
83 use std::collections::HashMap;
84
85 let mut bboxes: HashMap<(u32, u32), (usize, usize, usize, usize)> = HashMap::new();
87
88 for row in 0..rows {
89 let row_start = row * cols;
90 if row_start >= cells.len() {
91 break;
92 }
93 let row_end = (row_start + cols).min(cells.len());
94 for (col_off, cell) in cells[row_start..row_end].iter().enumerate() {
95 let Some((image_id, placement_id, _r_idx, _c_idx)) = decode_placeholder_cell(cell)
96 else {
97 continue;
98 };
99 let col = col_off;
100 bboxes
101 .entry((image_id, placement_id))
102 .and_modify(|b| {
103 if col < b.0 {
104 b.0 = col;
105 }
106 if row < b.1 {
107 b.1 = row;
108 }
109 if col > b.2 {
110 b.2 = col;
111 }
112 if row > b.3 {
113 b.3 = row;
114 }
115 })
116 .or_insert((col, row, col, row));
117 }
118 }
119
120 let mut hits: Vec<VirtualPlacementHit> = bboxes
121 .into_iter()
122 .map(
123 |((image_id, placement_id), (min_c, min_r, max_c, max_r))| VirtualPlacementHit {
124 image_id,
125 placement_id,
126 start_col: min_c,
127 start_row: min_r,
128 width_cells: max_c - min_c + 1,
129 height_cells: max_r - min_r + 1,
130 },
131 )
132 .collect();
133 hits.sort_by_key(|h| (h.image_id, h.placement_id, h.start_row, h.start_col));
135 hits
136}
137
138impl Renderer {
139 pub fn update_graphics(
147 &mut self,
148 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
149 view_scroll_offset: usize,
150 scrollback_len: usize,
151 visible_rows: usize,
152 ) -> Result<()> {
153 let had_graphics = !self.sixel_graphics.is_empty();
155
156 self.sixel_graphics.clear();
158
159 let total_lines = scrollback_len + visible_rows;
164 let view_end = total_lines.saturating_sub(view_scroll_offset);
165 let view_start = view_end.saturating_sub(visible_rows);
166
167 for graphic in graphics {
169 let id = graphic.id;
171 let (col, row) = graphic.position;
172
173 let core_cell_height = graphic
177 .cell_dimensions
178 .map(|(_, h)| h as f32)
179 .unwrap_or(2.0)
180 .max(1.0);
181 let display_cell_height = self.cell_renderer.cell_height().max(1.0);
182 let scroll_offset_in_display_rows = (graphic.scroll_offset_rows as f32
183 * core_cell_height
184 / display_cell_height)
185 .round() as usize;
186
187 let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
189 sb_row as isize - view_start as isize
192 } else {
193 let absolute_row =
197 scrollback_len.saturating_sub(scroll_offset_in_display_rows) + row;
198
199 log::trace!(
200 "[RENDERER] CALC: scrollback_len={}, row={}, scroll_offset_rows={}, scroll_in_display_rows={}, absolute_row={}, view_start={}, screen_row={}",
201 scrollback_len,
202 row,
203 graphic.scroll_offset_rows,
204 scroll_offset_in_display_rows,
205 absolute_row,
206 view_start,
207 absolute_row as isize - view_start as isize
208 );
209
210 absolute_row as isize - view_start as isize
211 };
212
213 log::debug!(
214 "[RENDERER] Graphics update: id={}, protocol={:?}, pos=({},{}), screen_row={}, scrollback_row={:?}, scroll_offset_rows={}, size={}x{}, view=[{},{})",
215 id,
216 graphic.protocol,
217 col,
218 row,
219 screen_row,
220 graphic.scrollback_row,
221 graphic.scroll_offset_rows,
222 graphic.width,
223 graphic.height,
224 view_start,
225 view_end
226 );
227
228 self.graphics_renderer.get_or_create_texture(
230 self.cell_renderer.device(),
231 self.cell_renderer.queue(),
232 id,
233 &graphic.pixels, graphic.width as u32,
235 graphic.height as u32,
236 )?;
237
238 let width_cells =
241 ((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
242 let height_cells =
243 ((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
244
245 let effective_clip_rows = if screen_row < 0 {
249 (-screen_row) as usize
250 } else {
251 0
252 };
253
254 self.sixel_graphics.push(GraphicRenderInfo {
255 id,
256 screen_row,
257 col,
258 width_cells,
259 height_cells,
260 alpha: 1.0,
261 scroll_offset_rows: effective_clip_rows,
262 });
263 }
264
265 if !graphics.is_empty() || had_graphics {
267 self.dirty = true;
268 }
269
270 Ok(())
271 }
272
273 pub fn update_pane_graphics(
280 &mut self,
281 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
282 view_scroll_offset: usize,
283 scrollback_len: usize,
284 visible_rows: usize,
285 ) -> Result<Vec<GraphicRenderInfo>> {
286 let total_lines = scrollback_len + visible_rows;
287 let view_end = total_lines.saturating_sub(view_scroll_offset);
288 let view_start = view_end.saturating_sub(visible_rows);
289
290 log::debug!(
291 "[PANE_GRAPHICS] update_pane_graphics: scrollback_len={}, visible_rows={}, view_scroll_offset={}, total_lines={}, view_start={}, view_end={}, graphics_count={}",
292 scrollback_len,
293 visible_rows,
294 view_scroll_offset,
295 total_lines,
296 view_start,
297 view_end,
298 graphics.len()
299 );
300
301 let mut positioned = Vec::new();
302
303 for graphic in graphics {
304 let id = graphic.id;
305 let (col, row) = graphic.position;
306
307 let core_cell_height = graphic
312 .cell_dimensions
313 .map(|(_, h)| h as f32)
314 .unwrap_or(2.0)
315 .max(1.0);
316 let display_cell_height = self.cell_renderer.cell_height().max(1.0);
317 let scroll_offset_in_display_rows = (graphic.scroll_offset_rows as f32
318 * core_cell_height
319 / display_cell_height)
320 .round() as usize;
321
322 let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
323 let sr = sb_row as isize - view_start as isize;
324 log::debug!(
325 "[PANE_GRAPHICS] scrollback graphic id={}: sb_row={}, view_start={}, screen_row={}",
326 id,
327 sb_row,
328 view_start,
329 sr
330 );
331 sr
332 } else {
333 let absolute_row =
334 scrollback_len.saturating_sub(scroll_offset_in_display_rows) + row;
335 let sr = absolute_row as isize - view_start as isize;
336 log::debug!(
337 "[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={}",
338 id,
339 scrollback_len,
340 graphic.scroll_offset_rows,
341 core_cell_height,
342 display_cell_height,
343 scroll_offset_in_display_rows,
344 row,
345 absolute_row,
346 view_start,
347 sr
348 );
349 sr
350 };
351
352 self.graphics_renderer.get_or_create_texture(
354 self.cell_renderer.device(),
355 self.cell_renderer.queue(),
356 id,
357 &graphic.pixels,
358 graphic.width as u32,
359 graphic.height as u32,
360 )?;
361
362 let width_cells =
363 ((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
364 let height_cells =
365 ((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
366
367 let effective_clip_rows = if screen_row < 0 {
368 (-screen_row) as usize
369 } else {
370 0
371 };
372
373 positioned.push(GraphicRenderInfo {
374 id,
375 screen_row,
376 col,
377 width_cells,
378 height_cells,
379 alpha: 1.0,
380 scroll_offset_rows: effective_clip_rows,
381 });
382 }
383
384 Ok(positioned)
385 }
386
387 pub(crate) fn update_pane_virtual_placements(
395 &mut self,
396 cells: &[Cell],
397 cols: usize,
398 rows: usize,
399 virtual_placements: &[TerminalGraphic],
400 ) -> Result<Vec<GraphicRenderInfo>> {
401 let hits = scan_placeholder_cells(cells, cols, rows);
402 if hits.is_empty() {
403 return Ok(Vec::new());
404 }
405
406 let mut out = Vec::with_capacity(hits.len());
407 for hit in hits {
408 let graphic = virtual_placements
412 .iter()
413 .find(|g| {
414 g.kitty_image_id == Some(hit.image_id)
415 && g.kitty_placement_id.unwrap_or(0) == hit.placement_id
416 })
417 .or_else(|| {
418 if hit.placement_id == 0 {
419 virtual_placements
420 .iter()
421 .find(|g| g.kitty_image_id == Some(hit.image_id))
422 } else {
423 None
424 }
425 });
426 let Some(graphic) = graphic else {
427 log::trace!(
428 "[VPLACE] no virtual placement for image_id={}, placement_id={}",
429 hit.image_id,
430 hit.placement_id
431 );
432 continue;
433 };
434
435 let cache_id = virtual_placement_cache_id(hit.image_id, hit.placement_id);
436 self.graphics_renderer.get_or_create_texture(
437 self.cell_renderer.device(),
438 self.cell_renderer.queue(),
439 cache_id,
440 &graphic.pixels,
441 graphic.width as u32,
442 graphic.height as u32,
443 )?;
444
445 out.push(GraphicRenderInfo {
446 id: cache_id,
447 screen_row: hit.start_row as isize,
448 col: hit.start_col,
449 width_cells: hit.width_cells,
450 height_cells: hit.height_cells,
451 alpha: 1.0,
452 scroll_offset_rows: 0,
453 });
454 }
455 Ok(out)
456 }
457
458 #[allow(clippy::too_many_arguments)]
464 pub(crate) fn render_pane_sixel_graphics(
465 &mut self,
466 surface_view: &wgpu::TextureView,
467 viewport: &crate::cell_renderer::PaneViewport,
468 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
469 scroll_offset: usize,
470 scrollback_len: usize,
471 visible_rows: usize,
472 cells: &[Cell],
473 cols: usize,
474 virtual_placements: &[TerminalGraphic],
475 ) -> Result<()> {
476 let mut positioned =
477 self.update_pane_graphics(graphics, scroll_offset, scrollback_len, visible_rows)?;
478
479 if !virtual_placements.is_empty() && !cells.is_empty() && cols > 0 {
484 positioned.extend(self.update_pane_virtual_placements(
485 cells,
486 cols,
487 visible_rows,
488 virtual_placements,
489 )?);
490 }
491
492 if positioned.is_empty() {
493 return Ok(());
494 }
495
496 let mut encoder =
497 self.cell_renderer
498 .device()
499 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
500 label: Some("pane sixel encoder"),
501 });
502
503 {
504 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
505 label: Some("pane sixel render pass"),
506 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
507 view: surface_view,
508 resolve_target: None,
509 ops: wgpu::Operations {
510 load: wgpu::LoadOp::Load,
511 store: wgpu::StoreOp::Store,
512 },
513 depth_slice: None,
514 })],
515 depth_stencil_attachment: None,
516 timestamp_writes: None,
517 occlusion_query_set: None,
518 multiview_mask: None,
519 });
520
521 let (sx, sy, sw, sh) = viewport.to_scissor_rect();
523 render_pass.set_scissor_rect(sx, sy, sw, sh);
524
525 let (ox, oy) = viewport.content_origin();
526
527 log::debug!(
528 "[PANE_GRAPHICS] render_pane_sixel_graphics: scissor=({},{},{},{}), origin=({},{}), window={}x{}, positioned_count={}",
529 sx,
530 sy,
531 sw,
532 sh,
533 ox,
534 oy,
535 self.size.width,
536 self.size.height,
537 positioned.len()
538 );
539 for g in &positioned {
540 log::debug!(
541 "[PANE_GRAPHICS] positioned: id={}, screen_row={}, col={}, width_cells={}, height_cells={}, clip_rows={}",
542 g.id,
543 g.screen_row,
544 g.col,
545 g.width_cells,
546 g.height_cells,
547 g.scroll_offset_rows
548 );
549 }
550
551 self.graphics_renderer.render_for_pane(
552 self.cell_renderer.device(),
553 self.cell_renderer.queue(),
554 &mut render_pass,
555 &positioned,
556 crate::graphics_renderer::PaneRenderGeometry {
557 window_width: self.size.width as f32,
558 window_height: self.size.height as f32,
559 pane_origin_x: ox,
560 pane_origin_y: oy,
561 },
562 )?;
563 }
564
565 self.cell_renderer
566 .queue()
567 .submit(std::iter::once(encoder.finish()));
568
569 Ok(())
570 }
571
572 pub fn clear_sixel_cache(&mut self) {
574 self.graphics_renderer.clear_cache();
575 self.sixel_graphics.clear();
576 self.dirty = true;
577 }
578
579 pub fn sixel_cache_size(&self) -> usize {
581 self.graphics_renderer.cache_size()
582 }
583
584 pub fn remove_sixel_texture(&mut self, id: u64) {
586 self.graphics_renderer.remove_texture(id);
587 self.sixel_graphics.retain(|g| g.id != id);
588 self.dirty = true;
589 }
590}
591
592#[cfg(test)]
593mod virtual_placement_tests {
594 use super::{
602 VIRTUAL_PLACEMENT_ID_FLAG, decode_placeholder_cell, scan_placeholder_cells,
603 virtual_placement_cache_id,
604 };
605 use crate::cell_renderer::Cell;
606 use par_term_emu_core_rust::graphics::placeholder::{
607 PLACEHOLDER_CHAR, create_placeholder_with_diacritics,
608 };
609
610 fn placeholder_cell(image_id: u32, row_idx: u16, col_idx: u16) -> Cell {
613 let r = ((image_id >> 16) & 0xFF) as u8;
614 let g = ((image_id >> 8) & 0xFF) as u8;
615 let b = (image_id & 0xFF) as u8;
616 Cell {
617 grapheme: create_placeholder_with_diacritics(row_idx, col_idx, None),
618 fg_color: [r, g, b, 255],
619 ..Default::default()
620 }
621 }
622
623 fn blank_cell() -> Cell {
624 Cell {
625 grapheme: " ".to_string(),
626 ..Default::default()
627 }
628 }
629
630 fn make_grid(cells: Vec<Cell>, cols: usize) -> (Vec<Cell>, usize, usize) {
631 let rows = cells.len() / cols;
632 (cells, cols, rows)
633 }
634
635 #[test]
636 fn decode_placeholder_recovers_image_id_and_indices() {
637 let cell = placeholder_cell(0x123456, 3, 7);
638 let (image_id, placement_id, row, col) = decode_placeholder_cell(&cell).unwrap();
639 assert_eq!(image_id, 0x123456);
640 assert_eq!(placement_id, 0);
641 assert_eq!(row, 3);
642 assert_eq!(col, 7);
643 }
644
645 #[test]
646 fn decode_placeholder_rejects_non_placeholder_cells() {
647 let cell = blank_cell();
648 assert!(decode_placeholder_cell(&cell).is_none());
649
650 let mut letter = blank_cell();
651 letter.grapheme = "a".to_string();
652 assert!(decode_placeholder_cell(&letter).is_none());
653 }
654
655 #[test]
656 fn scan_finds_single_rectangle_for_single_image() {
657 let mut cells = vec![blank_cell(); 4 * 3];
663 for r in 0..2 {
664 for c in 1..4 {
665 cells[r * 4 + c] = placeholder_cell(42, r as u16, (c - 1) as u16);
666 }
667 }
668 let (cells, cols, rows) = make_grid(cells, 4);
669
670 let hits = scan_placeholder_cells(&cells, cols, rows);
671 assert_eq!(hits.len(), 1);
672 let h = hits[0];
673 assert_eq!(h.image_id, 42);
674 assert_eq!(h.placement_id, 0);
675 assert_eq!(h.start_col, 1);
676 assert_eq!(h.start_row, 0);
677 assert_eq!(h.width_cells, 3);
678 assert_eq!(h.height_cells, 2);
679 }
680
681 #[test]
682 fn scan_groups_two_adjacent_images_separately() {
683 let mut cells = Vec::with_capacity(6);
685 for c in 0..3 {
686 cells.push(placeholder_cell(7, 0, c as u16));
687 }
688 for c in 0..3 {
689 cells.push(placeholder_cell(99, 0, c as u16));
690 }
691 let (cells, cols, rows) = make_grid(cells, 6);
692
693 let hits = scan_placeholder_cells(&cells, cols, rows);
694 assert_eq!(hits.len(), 2);
695
696 let h7 = hits.iter().find(|h| h.image_id == 7).unwrap();
697 assert_eq!(h7.start_col, 0);
698 assert_eq!(h7.width_cells, 3);
699 assert_eq!(h7.height_cells, 1);
700
701 let h99 = hits.iter().find(|h| h.image_id == 99).unwrap();
702 assert_eq!(h99.start_col, 3);
703 assert_eq!(h99.width_cells, 3);
704 assert_eq!(h99.height_cells, 1);
705 }
706
707 #[test]
708 fn scan_ignores_non_placeholder_cells() {
709 let cells = vec![blank_cell(); 6];
716 let hits = scan_placeholder_cells(&cells, 6, 1);
717 assert!(hits.is_empty());
718 }
719
720 #[test]
721 fn glyph_path_recognizes_placeholder_char() {
722 let cell = placeholder_cell(1, 0, 0);
727 let first = cell.grapheme.chars().next().unwrap();
728 assert_eq!(first, '\u{10EEEE}');
729 assert_eq!(first, PLACEHOLDER_CHAR);
730 }
731
732 #[test]
733 fn cache_id_is_disjoint_from_normal_graphic_ids() {
734 let id_a = virtual_placement_cache_id(42, 0);
738 let id_b = virtual_placement_cache_id(42, 1);
739 assert_ne!(id_a, id_b);
740 assert!(id_a & VIRTUAL_PLACEMENT_ID_FLAG != 0);
741 assert!(id_b & VIRTUAL_PLACEMENT_ID_FLAG != 0);
742 }
743}