use super::Renderer;
use crate::cell_renderer::Cell;
use crate::graphics_renderer::GraphicRenderInfo;
use anyhow::Result;
use par_term_emu_core_rust::graphics::TerminalGraphic;
use par_term_emu_core_rust::graphics::placeholder::{PLACEHOLDER_CHAR, diacritic_to_number};
const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
fn virtual_placement_cache_id(image_id: u32, placement_id: u32) -> u64 {
VIRTUAL_PLACEMENT_ID_FLAG | ((placement_id as u64) << 32) | image_id as u64
}
fn decode_placeholder_cell(cell: &Cell) -> Option<(u32, u32, u16, u16)> {
let mut chars = cell.grapheme.chars();
if chars.next()? != PLACEHOLDER_CHAR {
return None;
}
let row_idx = diacritic_to_number(chars.next()?)?;
let col_idx = diacritic_to_number(chars.next()?)?;
let msb_u8 = chars
.next()
.and_then(diacritic_to_number)
.map(|n| if n <= u8::MAX as u16 { n as u8 } else { 0 })
.unwrap_or(0);
let [r, g, b, _a] = cell.fg_color;
let image_id = ((msb_u8 as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32;
Some((image_id, 0, row_idx, col_idx))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct VirtualPlacementHit {
pub image_id: u32,
pub placement_id: u32,
pub start_col: usize,
pub start_row: usize,
pub width_cells: usize,
pub height_cells: usize,
}
pub(crate) fn scan_placeholder_cells(
cells: &[Cell],
cols: usize,
rows: usize,
) -> Vec<VirtualPlacementHit> {
use std::collections::HashMap;
let mut bboxes: HashMap<(u32, u32), (usize, usize, usize, usize)> = HashMap::new();
for row in 0..rows {
let row_start = row * cols;
if row_start >= cells.len() {
break;
}
let row_end = (row_start + cols).min(cells.len());
for (col_off, cell) in cells[row_start..row_end].iter().enumerate() {
let Some((image_id, placement_id, _r_idx, _c_idx)) = decode_placeholder_cell(cell)
else {
continue;
};
let col = col_off;
bboxes
.entry((image_id, placement_id))
.and_modify(|b| {
if col < b.0 {
b.0 = col;
}
if row < b.1 {
b.1 = row;
}
if col > b.2 {
b.2 = col;
}
if row > b.3 {
b.3 = row;
}
})
.or_insert((col, row, col, row));
}
}
let mut hits: Vec<VirtualPlacementHit> = bboxes
.into_iter()
.map(
|((image_id, placement_id), (min_c, min_r, max_c, max_r))| VirtualPlacementHit {
image_id,
placement_id,
start_col: min_c,
start_row: min_r,
width_cells: max_c - min_c + 1,
height_cells: max_r - min_r + 1,
},
)
.collect();
hits.sort_by_key(|h| (h.image_id, h.placement_id, h.start_row, h.start_col));
hits
}
impl Renderer {
pub fn update_graphics(
&mut self,
graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
view_scroll_offset: usize,
scrollback_len: usize,
visible_rows: usize,
) -> Result<()> {
let had_graphics = !self.sixel_graphics.is_empty();
self.sixel_graphics.clear();
let total_lines = scrollback_len + visible_rows;
let view_end = total_lines.saturating_sub(view_scroll_offset);
let view_start = view_end.saturating_sub(visible_rows);
for graphic in graphics {
let id = graphic.id;
let (col, row) = graphic.position;
let core_cell_height = graphic
.cell_dimensions
.map(|(_, h)| h as f32)
.unwrap_or(2.0)
.max(1.0);
let display_cell_height = self.cell_renderer.cell_height().max(1.0);
let scroll_offset_in_display_rows = (graphic.scroll_offset_rows as f32
* core_cell_height
/ display_cell_height)
.round() as usize;
let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
sb_row as isize - view_start as isize
} else {
let absolute_row =
scrollback_len.saturating_sub(scroll_offset_in_display_rows) + row;
log::trace!(
"[RENDERER] CALC: scrollback_len={}, row={}, scroll_offset_rows={}, scroll_in_display_rows={}, absolute_row={}, view_start={}, screen_row={}",
scrollback_len,
row,
graphic.scroll_offset_rows,
scroll_offset_in_display_rows,
absolute_row,
view_start,
absolute_row as isize - view_start as isize
);
absolute_row as isize - view_start as isize
};
log::debug!(
"[RENDERER] Graphics update: id={}, protocol={:?}, pos=({},{}), screen_row={}, scrollback_row={:?}, scroll_offset_rows={}, size={}x{}, view=[{},{})",
id,
graphic.protocol,
col,
row,
screen_row,
graphic.scrollback_row,
graphic.scroll_offset_rows,
graphic.width,
graphic.height,
view_start,
view_end
);
self.graphics_renderer.get_or_create_texture(
self.cell_renderer.device(),
self.cell_renderer.queue(),
id,
&graphic.pixels, graphic.width as u32,
graphic.height as u32,
)?;
let width_cells =
((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
let height_cells =
((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
let effective_clip_rows = if screen_row < 0 {
(-screen_row) as usize
} else {
0
};
self.sixel_graphics.push(GraphicRenderInfo {
id,
screen_row,
col,
width_cells,
height_cells,
alpha: 1.0,
scroll_offset_rows: effective_clip_rows,
});
}
if !graphics.is_empty() || had_graphics {
self.dirty = true;
}
Ok(())
}
pub fn update_pane_graphics(
&mut self,
graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
view_scroll_offset: usize,
scrollback_len: usize,
visible_rows: usize,
) -> Result<Vec<GraphicRenderInfo>> {
let total_lines = scrollback_len + visible_rows;
let view_end = total_lines.saturating_sub(view_scroll_offset);
let view_start = view_end.saturating_sub(visible_rows);
log::debug!(
"[PANE_GRAPHICS] update_pane_graphics: scrollback_len={}, visible_rows={}, view_scroll_offset={}, total_lines={}, view_start={}, view_end={}, graphics_count={}",
scrollback_len,
visible_rows,
view_scroll_offset,
total_lines,
view_start,
view_end,
graphics.len()
);
let mut positioned = Vec::new();
for graphic in graphics {
let id = graphic.id;
let (col, row) = graphic.position;
let core_cell_height = graphic
.cell_dimensions
.map(|(_, h)| h as f32)
.unwrap_or(2.0)
.max(1.0);
let display_cell_height = self.cell_renderer.cell_height().max(1.0);
let scroll_offset_in_display_rows = (graphic.scroll_offset_rows as f32
* core_cell_height
/ display_cell_height)
.round() as usize;
let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
let sr = sb_row as isize - view_start as isize;
log::debug!(
"[PANE_GRAPHICS] scrollback graphic id={}: sb_row={}, view_start={}, screen_row={}",
id,
sb_row,
view_start,
sr
);
sr
} else {
let absolute_row =
scrollback_len.saturating_sub(scroll_offset_in_display_rows) + row;
let sr = absolute_row as isize - view_start as isize;
log::debug!(
"[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={}",
id,
scrollback_len,
graphic.scroll_offset_rows,
core_cell_height,
display_cell_height,
scroll_offset_in_display_rows,
row,
absolute_row,
view_start,
sr
);
sr
};
self.graphics_renderer.get_or_create_texture(
self.cell_renderer.device(),
self.cell_renderer.queue(),
id,
&graphic.pixels,
graphic.width as u32,
graphic.height as u32,
)?;
let width_cells =
((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
let height_cells =
((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
let effective_clip_rows = if screen_row < 0 {
(-screen_row) as usize
} else {
0
};
positioned.push(GraphicRenderInfo {
id,
screen_row,
col,
width_cells,
height_cells,
alpha: 1.0,
scroll_offset_rows: effective_clip_rows,
});
}
Ok(positioned)
}
pub(crate) fn update_pane_virtual_placements(
&mut self,
cells: &[Cell],
cols: usize,
rows: usize,
virtual_placements: &[TerminalGraphic],
) -> Result<Vec<GraphicRenderInfo>> {
let hits = scan_placeholder_cells(cells, cols, rows);
if hits.is_empty() {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(hits.len());
for hit in hits {
let graphic = virtual_placements
.iter()
.find(|g| {
g.kitty_image_id == Some(hit.image_id)
&& g.kitty_placement_id.unwrap_or(0) == hit.placement_id
})
.or_else(|| {
if hit.placement_id == 0 {
virtual_placements
.iter()
.find(|g| g.kitty_image_id == Some(hit.image_id))
} else {
None
}
});
let Some(graphic) = graphic else {
log::trace!(
"[VPLACE] no virtual placement for image_id={}, placement_id={}",
hit.image_id,
hit.placement_id
);
continue;
};
let cache_id = virtual_placement_cache_id(hit.image_id, hit.placement_id);
self.graphics_renderer.get_or_create_texture(
self.cell_renderer.device(),
self.cell_renderer.queue(),
cache_id,
&graphic.pixels,
graphic.width as u32,
graphic.height as u32,
)?;
out.push(GraphicRenderInfo {
id: cache_id,
screen_row: hit.start_row as isize,
col: hit.start_col,
width_cells: hit.width_cells,
height_cells: hit.height_cells,
alpha: 1.0,
scroll_offset_rows: 0,
});
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render_pane_sixel_graphics(
&mut self,
surface_view: &wgpu::TextureView,
viewport: &crate::cell_renderer::PaneViewport,
graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
scroll_offset: usize,
scrollback_len: usize,
visible_rows: usize,
cells: &[Cell],
cols: usize,
virtual_placements: &[TerminalGraphic],
) -> Result<()> {
let mut positioned =
self.update_pane_graphics(graphics, scroll_offset, scrollback_len, visible_rows)?;
if !virtual_placements.is_empty() && !cells.is_empty() && cols > 0 {
positioned.extend(self.update_pane_virtual_placements(
cells,
cols,
visible_rows,
virtual_placements,
)?);
}
if positioned.is_empty() {
return Ok(());
}
let mut encoder =
self.cell_renderer
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("pane sixel encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("pane sixel render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
let (sx, sy, sw, sh) = viewport.to_scissor_rect();
render_pass.set_scissor_rect(sx, sy, sw, sh);
let (ox, oy) = viewport.content_origin();
log::debug!(
"[PANE_GRAPHICS] render_pane_sixel_graphics: scissor=({},{},{},{}), origin=({},{}), window={}x{}, positioned_count={}",
sx,
sy,
sw,
sh,
ox,
oy,
self.size.width,
self.size.height,
positioned.len()
);
for g in &positioned {
log::debug!(
"[PANE_GRAPHICS] positioned: id={}, screen_row={}, col={}, width_cells={}, height_cells={}, clip_rows={}",
g.id,
g.screen_row,
g.col,
g.width_cells,
g.height_cells,
g.scroll_offset_rows
);
}
self.graphics_renderer.render_for_pane(
self.cell_renderer.device(),
self.cell_renderer.queue(),
&mut render_pass,
&positioned,
crate::graphics_renderer::PaneRenderGeometry {
window_width: self.size.width as f32,
window_height: self.size.height as f32,
pane_origin_x: ox,
pane_origin_y: oy,
},
)?;
}
self.cell_renderer
.queue()
.submit(std::iter::once(encoder.finish()));
Ok(())
}
pub fn clear_sixel_cache(&mut self) {
self.graphics_renderer.clear_cache();
self.sixel_graphics.clear();
self.dirty = true;
}
pub fn sixel_cache_size(&self) -> usize {
self.graphics_renderer.cache_size()
}
pub fn remove_sixel_texture(&mut self, id: u64) {
self.graphics_renderer.remove_texture(id);
self.sixel_graphics.retain(|g| g.id != id);
self.dirty = true;
}
}
#[cfg(test)]
mod virtual_placement_tests {
use super::{
VIRTUAL_PLACEMENT_ID_FLAG, decode_placeholder_cell, scan_placeholder_cells,
virtual_placement_cache_id,
};
use crate::cell_renderer::Cell;
use par_term_emu_core_rust::graphics::placeholder::{
PLACEHOLDER_CHAR, create_placeholder_with_diacritics,
};
fn placeholder_cell(image_id: u32, row_idx: u16, col_idx: u16) -> Cell {
let r = ((image_id >> 16) & 0xFF) as u8;
let g = ((image_id >> 8) & 0xFF) as u8;
let b = (image_id & 0xFF) as u8;
Cell {
grapheme: create_placeholder_with_diacritics(row_idx, col_idx, None),
fg_color: [r, g, b, 255],
..Default::default()
}
}
fn blank_cell() -> Cell {
Cell {
grapheme: " ".to_string(),
..Default::default()
}
}
fn make_grid(cells: Vec<Cell>, cols: usize) -> (Vec<Cell>, usize, usize) {
let rows = cells.len() / cols;
(cells, cols, rows)
}
#[test]
fn decode_placeholder_recovers_image_id_and_indices() {
let cell = placeholder_cell(0x123456, 3, 7);
let (image_id, placement_id, row, col) = decode_placeholder_cell(&cell).unwrap();
assert_eq!(image_id, 0x123456);
assert_eq!(placement_id, 0);
assert_eq!(row, 3);
assert_eq!(col, 7);
}
#[test]
fn decode_placeholder_rejects_non_placeholder_cells() {
let cell = blank_cell();
assert!(decode_placeholder_cell(&cell).is_none());
let mut letter = blank_cell();
letter.grapheme = "a".to_string();
assert!(decode_placeholder_cell(&letter).is_none());
}
#[test]
fn scan_finds_single_rectangle_for_single_image() {
let mut cells = vec![blank_cell(); 4 * 3];
for r in 0..2 {
for c in 1..4 {
cells[r * 4 + c] = placeholder_cell(42, r as u16, (c - 1) as u16);
}
}
let (cells, cols, rows) = make_grid(cells, 4);
let hits = scan_placeholder_cells(&cells, cols, rows);
assert_eq!(hits.len(), 1);
let h = hits[0];
assert_eq!(h.image_id, 42);
assert_eq!(h.placement_id, 0);
assert_eq!(h.start_col, 1);
assert_eq!(h.start_row, 0);
assert_eq!(h.width_cells, 3);
assert_eq!(h.height_cells, 2);
}
#[test]
fn scan_groups_two_adjacent_images_separately() {
let mut cells = Vec::with_capacity(6);
for c in 0..3 {
cells.push(placeholder_cell(7, 0, c as u16));
}
for c in 0..3 {
cells.push(placeholder_cell(99, 0, c as u16));
}
let (cells, cols, rows) = make_grid(cells, 6);
let hits = scan_placeholder_cells(&cells, cols, rows);
assert_eq!(hits.len(), 2);
let h7 = hits.iter().find(|h| h.image_id == 7).unwrap();
assert_eq!(h7.start_col, 0);
assert_eq!(h7.width_cells, 3);
assert_eq!(h7.height_cells, 1);
let h99 = hits.iter().find(|h| h.image_id == 99).unwrap();
assert_eq!(h99.start_col, 3);
assert_eq!(h99.width_cells, 3);
assert_eq!(h99.height_cells, 1);
}
#[test]
fn scan_ignores_non_placeholder_cells() {
let cells = vec![blank_cell(); 6];
let hits = scan_placeholder_cells(&cells, 6, 1);
assert!(hits.is_empty());
}
#[test]
fn glyph_path_recognizes_placeholder_char() {
let cell = placeholder_cell(1, 0, 0);
let first = cell.grapheme.chars().next().unwrap();
assert_eq!(first, '\u{10EEEE}');
assert_eq!(first, PLACEHOLDER_CHAR);
}
#[test]
fn cache_id_is_disjoint_from_normal_graphic_ids() {
let id_a = virtual_placement_cache_id(42, 0);
let id_b = virtual_placement_cache_id(42, 1);
assert_ne!(id_a, id_b);
assert!(id_a & VIRTUAL_PLACEMENT_ID_FLAG != 0);
assert!(id_b & VIRTUAL_PLACEMENT_ID_FLAG != 0);
}
}