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 });
519
520 let (sx, sy, sw, sh) = viewport.to_scissor_rect();
522 render_pass.set_scissor_rect(sx, sy, sw, sh);
523
524 let (ox, oy) = viewport.content_origin();
525
526 log::debug!(
527 "[PANE_GRAPHICS] render_pane_sixel_graphics: scissor=({},{},{},{}), origin=({},{}), window={}x{}, positioned_count={}",
528 sx,
529 sy,
530 sw,
531 sh,
532 ox,
533 oy,
534 self.size.width,
535 self.size.height,
536 positioned.len()
537 );
538 for g in &positioned {
539 log::debug!(
540 "[PANE_GRAPHICS] positioned: id={}, screen_row={}, col={}, width_cells={}, height_cells={}, clip_rows={}",
541 g.id,
542 g.screen_row,
543 g.col,
544 g.width_cells,
545 g.height_cells,
546 g.scroll_offset_rows
547 );
548 }
549
550 self.graphics_renderer.render_for_pane(
551 self.cell_renderer.device(),
552 self.cell_renderer.queue(),
553 &mut render_pass,
554 &positioned,
555 crate::graphics_renderer::PaneRenderGeometry {
556 window_width: self.size.width as f32,
557 window_height: self.size.height as f32,
558 pane_origin_x: ox,
559 pane_origin_y: oy,
560 },
561 )?;
562 }
563
564 self.cell_renderer
565 .queue()
566 .submit(std::iter::once(encoder.finish()));
567
568 Ok(())
569 }
570
571 pub fn clear_sixel_cache(&mut self) {
573 self.graphics_renderer.clear_cache();
574 self.sixel_graphics.clear();
575 self.dirty = true;
576 }
577
578 pub fn sixel_cache_size(&self) -> usize {
580 self.graphics_renderer.cache_size()
581 }
582
583 pub fn remove_sixel_texture(&mut self, id: u64) {
585 self.graphics_renderer.remove_texture(id);
586 self.sixel_graphics.retain(|g| g.id != id);
587 self.dirty = true;
588 }
589}
590
591#[cfg(test)]
592mod virtual_placement_tests {
593 use super::{
601 VIRTUAL_PLACEMENT_ID_FLAG, decode_placeholder_cell, scan_placeholder_cells,
602 virtual_placement_cache_id,
603 };
604 use crate::cell_renderer::Cell;
605 use par_term_emu_core_rust::graphics::placeholder::{
606 PLACEHOLDER_CHAR, create_placeholder_with_diacritics,
607 };
608
609 fn placeholder_cell(image_id: u32, row_idx: u16, col_idx: u16) -> Cell {
612 let r = ((image_id >> 16) & 0xFF) as u8;
613 let g = ((image_id >> 8) & 0xFF) as u8;
614 let b = (image_id & 0xFF) as u8;
615 Cell {
616 grapheme: create_placeholder_with_diacritics(row_idx, col_idx, None),
617 fg_color: [r, g, b, 255],
618 ..Default::default()
619 }
620 }
621
622 fn blank_cell() -> Cell {
623 Cell {
624 grapheme: " ".to_string(),
625 ..Default::default()
626 }
627 }
628
629 fn make_grid(cells: Vec<Cell>, cols: usize) -> (Vec<Cell>, usize, usize) {
630 let rows = cells.len() / cols;
631 (cells, cols, rows)
632 }
633
634 #[test]
635 fn decode_placeholder_recovers_image_id_and_indices() {
636 let cell = placeholder_cell(0x123456, 3, 7);
637 let (image_id, placement_id, row, col) = decode_placeholder_cell(&cell).unwrap();
638 assert_eq!(image_id, 0x123456);
639 assert_eq!(placement_id, 0);
640 assert_eq!(row, 3);
641 assert_eq!(col, 7);
642 }
643
644 #[test]
645 fn decode_placeholder_rejects_non_placeholder_cells() {
646 let cell = blank_cell();
647 assert!(decode_placeholder_cell(&cell).is_none());
648
649 let mut letter = blank_cell();
650 letter.grapheme = "a".to_string();
651 assert!(decode_placeholder_cell(&letter).is_none());
652 }
653
654 #[test]
655 fn scan_finds_single_rectangle_for_single_image() {
656 let mut cells = vec![blank_cell(); 4 * 3];
662 for r in 0..2 {
663 for c in 1..4 {
664 cells[r * 4 + c] = placeholder_cell(42, r as u16, (c - 1) as u16);
665 }
666 }
667 let (cells, cols, rows) = make_grid(cells, 4);
668
669 let hits = scan_placeholder_cells(&cells, cols, rows);
670 assert_eq!(hits.len(), 1);
671 let h = hits[0];
672 assert_eq!(h.image_id, 42);
673 assert_eq!(h.placement_id, 0);
674 assert_eq!(h.start_col, 1);
675 assert_eq!(h.start_row, 0);
676 assert_eq!(h.width_cells, 3);
677 assert_eq!(h.height_cells, 2);
678 }
679
680 #[test]
681 fn scan_groups_two_adjacent_images_separately() {
682 let mut cells = Vec::with_capacity(6);
684 for c in 0..3 {
685 cells.push(placeholder_cell(7, 0, c as u16));
686 }
687 for c in 0..3 {
688 cells.push(placeholder_cell(99, 0, c as u16));
689 }
690 let (cells, cols, rows) = make_grid(cells, 6);
691
692 let hits = scan_placeholder_cells(&cells, cols, rows);
693 assert_eq!(hits.len(), 2);
694
695 let h7 = hits.iter().find(|h| h.image_id == 7).unwrap();
696 assert_eq!(h7.start_col, 0);
697 assert_eq!(h7.width_cells, 3);
698 assert_eq!(h7.height_cells, 1);
699
700 let h99 = hits.iter().find(|h| h.image_id == 99).unwrap();
701 assert_eq!(h99.start_col, 3);
702 assert_eq!(h99.width_cells, 3);
703 assert_eq!(h99.height_cells, 1);
704 }
705
706 #[test]
707 fn scan_ignores_non_placeholder_cells() {
708 let cells = vec![blank_cell(); 6];
715 let hits = scan_placeholder_cells(&cells, 6, 1);
716 assert!(hits.is_empty());
717 }
718
719 #[test]
720 fn glyph_path_recognizes_placeholder_char() {
721 let cell = placeholder_cell(1, 0, 0);
726 let first = cell.grapheme.chars().next().unwrap();
727 assert_eq!(first, '\u{10EEEE}');
728 assert_eq!(first, PLACEHOLDER_CHAR);
729 }
730
731 #[test]
732 fn cache_id_is_disjoint_from_normal_graphic_ids() {
733 let id_a = virtual_placement_cache_id(42, 0);
737 let id_b = virtual_placement_cache_id(42, 1);
738 assert_ne!(id_a, id_b);
739 assert!(id_a & VIRTUAL_PLACEMENT_ID_FLAG != 0);
740 assert!(id_b & VIRTUAL_PLACEMENT_ID_FLAG != 0);
741 }
742}