use crate::error::RenderError;
use crate::gpu_utils;
use crate::wgpu_conversions::ImageScalingModeWgpu;
use par_term_config::ImageScalingMode;
use std::collections::HashMap;
use std::time::Instant;
use wgpu::*;
const MAX_TEXTURE_CACHE_SIZE: usize = 100;
const INITIAL_GRAPHICS_INSTANCE_CAPACITY: usize = 32;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SixelInstance {
position: [f32; 2], tex_coords: [f32; 4], size: [f32; 2], alpha: f32, _padding: f32, }
#[derive(Debug, Clone, Copy)]
pub struct PaneRenderGeometry {
pub window_width: f32,
pub window_height: f32,
pub pane_origin_x: f32,
pub pane_origin_y: f32,
}
#[allow(clippy::too_many_arguments)]
fn compute_graphic_geometry(
tex_w: f32,
tex_h: f32,
crop: [u32; 4],
width_cells: usize,
height_cells: usize,
cell_w: f32,
cell_h: f32,
clip_px: f32,
has_cols: bool,
has_rows: bool,
preserve_aspect: bool,
is_virtual: bool,
window_w: f32,
window_h: f32,
) -> ([f32; 4], [f32; 2]) {
let has_crop = crop != [0, 0, 0, 0];
let (sx, sy, sw, sh) = if has_crop && tex_w > 0.0 && tex_h > 0.0 {
let x = (crop[0] as f32).min(tex_w);
let y = (crop[1] as f32).min(tex_h);
let w = if crop[2] > 0 {
(crop[2] as f32).min(tex_w - x)
} else {
tex_w - x
};
let h = if crop[3] > 0 {
(crop[3] as f32).min(tex_h - y)
} else {
tex_h - y
};
(x, y, w.max(0.0), h.max(0.0))
} else {
(0.0, 0.0, tex_w, tex_h)
};
if sw <= 0.0 || sh <= 0.0 {
return ([0.0, 0.0, 0.0, 0.0], [0.0, 0.0]);
}
let aspect = sw / sh;
let (dest_w, dest_h) = if is_virtual || (has_cols && has_rows) {
(width_cells as f32 * cell_w, height_cells as f32 * cell_h)
} else if has_cols && !has_rows {
let dw = width_cells as f32 * cell_w;
(dw, dw / aspect)
} else if has_rows && !has_cols {
let dh = height_cells as f32 * cell_h;
(dh * aspect, dh)
} else if has_crop && sw > 0.0 && sh > 0.0 {
(sw, sh)
} else if preserve_aspect && tex_w > 0.0 && tex_h > 0.0 {
(tex_w, tex_h)
} else {
(width_cells as f32 * cell_w, height_cells as f32 * cell_h)
};
let visible_frac = if dest_h > 0.0 {
((dest_h - clip_px) / dest_h).clamp(0.0, 1.0)
} else {
0.0
};
let scrolled_frac = if dest_h > 0.0 {
(clip_px / dest_h).clamp(0.0, 1.0)
} else {
0.0
};
let uv = if sw > 0.0 && sh > 0.0 && tex_w > 0.0 && tex_h > 0.0 {
[
sx / tex_w,
(sy + sh * scrolled_frac) / tex_h,
sw / tex_w,
(sh * visible_frac) / tex_h,
]
} else {
[0.0, 0.0, 1.0, 1.0]
};
let size = (dest_w / window_w, dest_h * visible_frac / window_h);
(uv, size.into())
}
#[cfg(test)]
mod geometry_tests {
use super::compute_graphic_geometry;
const WW: f32 = 800.0;
const WH: f32 = 600.0;
const CW: f32 = 10.0;
const CH: f32 = 20.0;
#[test]
fn no_crop_both_cells_uses_dest_fraction_for_uv() {
let (uv, size) = compute_graphic_geometry(
100.0,
100.0,
[0, 0, 0, 0],
10,
2,
CW,
CH,
20.0, true,
true, false,
false, WW,
WH,
);
let expected_uv_y = 50.0 / 100.0;
let expected_uv_h = 50.0 / 100.0;
assert!((uv[1] - expected_uv_y).abs() < 1e-5);
assert!((uv[3] - expected_uv_h).abs() < 1e-5);
assert!((size[1] - 20.0 / WH).abs() < 1e-5);
}
#[test]
fn natural_crop_without_cells_uses_crop_height_for_dest() {
let (uv, size) = compute_graphic_geometry(
100.0,
100.0,
[0, 0, 0, 25],
1,
1,
CW,
CH,
20.0,
false,
false,
false,
false,
WW,
WH,
);
let expected_uv_y = (0.0 + 25.0 * 0.8) / 100.0;
let expected_uv_h = (25.0 * 0.2) / 100.0;
assert!((uv[1] - expected_uv_y).abs() < 1e-5);
assert!((uv[3] - expected_uv_h).abs() < 1e-5);
assert!((size[1] - 5.0 / WH).abs() < 1e-5);
}
#[test]
fn y_offset_produces_sub_row_clip() {
let top_px = -1.0 * CH + 5.0;
let clip_px = (-top_px).max(0.0);
assert_eq!(clip_px, 15.0);
let (uv, size) = compute_graphic_geometry(
100.0,
100.0,
[0, 0, 0, 0],
10,
3,
CW,
CH,
clip_px,
true,
true,
false,
false,
WW,
WH,
);
assert!((uv[1] - 25.0 / 100.0).abs() < 1e-5);
assert!((uv[3] - 75.0 / 100.0).abs() < 1e-5);
assert!((size[1] - 45.0 / WH).abs() < 1e-5);
}
#[test]
fn c_only_computes_exact_height_from_aspect() {
let (_uv, size) = compute_graphic_geometry(
100.0,
100.0,
[0, 0, 0, 0],
5,
3,
CW,
CH,
0.0,
true,
false, false,
false,
WW,
WH,
);
assert!((size[0] - 50.0 / WW).abs() < 1e-5);
assert!((size[1] - 50.0 / WH).abs() < 1e-5);
}
#[test]
fn r_only_computes_exact_width_from_aspect() {
let (_uv, size) = compute_graphic_geometry(
100.0,
100.0,
[0, 0, 0, 0],
4,
2,
CW,
CH,
0.0,
false,
true, false,
false,
WW,
WH,
);
assert!((size[0] - 40.0 / WW).abs() < 1e-5);
assert!((size[1] - 40.0 / WH).abs() < 1e-5);
}
#[test]
fn c_only_wide_source_computes_proportional_height() {
let (_uv, size) = compute_graphic_geometry(
100.0,
50.0,
[0, 0, 0, 0],
5,
1,
CW,
CH,
0.0,
true,
false,
false,
false,
WW,
WH,
);
assert!((size[0] - 50.0 / WW).abs() < 1e-5);
assert!((size[1] - 25.0 / WH).abs() < 1e-5);
}
#[test]
fn zero_size_crop_at_edge_returns_zero_output() {
let (uv, size) = compute_graphic_geometry(
100.0,
100.0,
[100, 0, 0, 0],
5,
3,
CW,
CH,
0.0,
true,
false,
false,
false,
WW,
WH,
);
assert_eq!(uv, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(size, [0.0, 0.0]);
}
}
#[derive(Debug, Clone, Copy)]
pub struct GraphicRenderInfo {
pub id: u64,
pub screen_row: isize,
pub col: usize,
pub width_cells: usize,
pub height_cells: usize,
pub alpha: f32,
pub scroll_offset_rows: usize,
pub destination_offset_x: u32,
pub destination_offset_y: u32,
pub source_crop: [u32; 4],
pub has_cols: bool,
pub has_rows: bool,
}
struct SixelTextureInfo {
texture: Texture,
#[allow(dead_code)] view: TextureView,
bind_group: BindGroup,
width: u32,
height: u32,
}
struct CachedTexture {
texture: SixelTextureInfo,
last_used: Instant,
}
pub struct GraphicsRenderer {
pipeline: RenderPipeline,
bind_group_layout: BindGroupLayout,
sampler: Sampler,
instance_buffer: Buffer,
instance_capacity: usize,
texture_cache: HashMap<u64, CachedTexture>,
cell_width: f32,
cell_height: f32,
window_padding: f32,
content_offset_y: f32,
content_offset_x: f32,
preserve_aspect_ratio: bool,
}
impl GraphicsRenderer {
pub fn new(
device: &Device,
surface_format: TextureFormat,
cell_width: f32,
cell_height: f32,
window_padding: f32,
scaling_mode: ImageScalingMode,
preserve_aspect_ratio: bool,
) -> Result<Self, RenderError> {
let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("Sixel Bind Group Layout"),
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
},
],
});
let sampler = gpu_utils::create_sampler_with_filter(
device,
scaling_mode.to_filter_mode(),
Some("Sixel Sampler"),
);
let pipeline = Self::create_pipeline(device, surface_format, &bind_group_layout)?;
let initial_capacity = INITIAL_GRAPHICS_INSTANCE_CAPACITY;
let instance_buffer = device.create_buffer(&BufferDescriptor {
label: Some("Sixel Instance Buffer"),
size: (initial_capacity * std::mem::size_of::<SixelInstance>()) as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Ok(Self {
pipeline,
bind_group_layout,
sampler,
instance_buffer,
instance_capacity: initial_capacity,
texture_cache: HashMap::new(),
cell_width,
cell_height,
window_padding,
content_offset_y: 0.0,
content_offset_x: 0.0,
preserve_aspect_ratio,
})
}
fn create_pipeline(
device: &Device,
format: TextureFormat,
bind_group_layout: &BindGroupLayout,
) -> Result<RenderPipeline, RenderError> {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Sixel Shader"),
source: ShaderSource::Wgsl(include_str!("shaders/sixel.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("Sixel Pipeline Layout"),
bind_group_layouts: &[Some(bind_group_layout)],
immediate_size: 0,
});
Ok(device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("Sixel Pipeline"),
layout: Some(&pipeline_layout),
vertex: VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Some(VertexBufferLayout {
array_stride: std::mem::size_of::<SixelInstance>() as u64,
step_mode: VertexStepMode::Instance,
attributes: &vertex_attr_array![
0 => Float32x2, 1 => Float32x4, 2 => Float32x2, 3 => Float32, ],
})],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format,
blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
multisample: MultisampleState::default(),
cache: None,
multiview_mask: None,
}))
}
pub fn get_or_create_texture(
&mut self,
device: &Device,
queue: &Queue,
id: u64,
rgba_data: &[u8],
width: u32,
height: u32,
) -> Result<(), RenderError> {
if let Some(cached) = self.texture_cache.get_mut(&id) {
cached.last_used = Instant::now();
const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
if id & VIRTUAL_PLACEMENT_ID_FLAG != 0 {
return Ok(());
}
let expected_size = (width * height * 4) as usize;
if rgba_data.len() != expected_size {
return Err(RenderError::InvalidTextureData {
expected: expected_size,
actual: rgba_data.len(),
});
}
queue.write_texture(
TexelCopyTextureInfo {
texture: &cached.texture.texture,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
rgba_data,
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
return Ok(());
}
let expected_size = (width * height * 4) as usize;
if rgba_data.len() != expected_size {
return Err(RenderError::InvalidTextureData {
expected: expected_size,
actual: rgba_data.len(),
});
}
if self.texture_cache.len() >= MAX_TEXTURE_CACHE_SIZE
&& let Some((&lru_id, _)) = self
.texture_cache
.iter()
.min_by_key(|(_, cached)| cached.last_used)
{
log::debug!(
"[GRAPHICS] Evicting LRU texture: id={}, cache_size={}",
lru_id,
self.texture_cache.len()
);
self.texture_cache.remove(&lru_id);
}
let texture = device.create_texture(&TextureDescriptor {
label: Some(&format!("Sixel Texture {}", id)),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
rgba_data,
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let view = texture.create_view(&TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&BindGroupDescriptor {
label: Some(&format!("Sixel Bind Group {}", id)),
layout: &self.bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&self.sampler),
},
],
});
self.texture_cache.insert(
id,
CachedTexture {
texture: SixelTextureInfo {
texture,
view,
bind_group,
width,
height,
},
last_used: Instant::now(),
},
);
log::debug!(
"[GRAPHICS] Created sixel texture: id={}, size={}x{}, cache_size={}/{}",
id,
width,
height,
self.texture_cache.len(),
MAX_TEXTURE_CACHE_SIZE
);
Ok(())
}
pub fn render(
&mut self,
device: &Device,
queue: &Queue,
render_pass: &mut RenderPass,
graphics: &[GraphicRenderInfo],
window_width: f32,
window_height: f32,
) -> Result<(), RenderError> {
if graphics.is_empty() {
return Ok(());
}
let mut instances = Vec::with_capacity(graphics.len());
for g in graphics {
let (
id,
row,
col,
_width_cells,
_height_cells,
alpha,
_scroll_offset_rows,
dest_off_x,
dest_off_y,
crop,
has_cols,
has_rows,
) = (
g.id,
g.screen_row,
g.col,
g.width_cells,
g.height_cells,
g.alpha,
g.scroll_offset_rows,
g.destination_offset_x,
g.destination_offset_y,
g.source_crop,
g.has_cols,
g.has_rows,
);
if let Some(cached) = self.texture_cache.get_mut(&id) {
cached.last_used = Instant::now();
let tex_info = &cached.texture;
let top_px = row as f32 * self.cell_height + dest_off_y as f32;
let clip_px = (-top_px).max(0.0);
let draw_y_px = top_px.max(0.0);
let x = (self.window_padding
+ self.content_offset_x
+ col as f32 * self.cell_width
+ dest_off_x as f32)
/ window_width;
let y = (self.window_padding + self.content_offset_y + draw_y_px) / window_height;
const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
let is_virtual_placement = id & VIRTUAL_PLACEMENT_ID_FLAG != 0;
let (tex_coords, size) = compute_graphic_geometry(
tex_info.width as f32,
tex_info.height as f32,
crop,
_width_cells,
_height_cells,
self.cell_width,
self.cell_height,
clip_px,
has_cols,
has_rows,
self.preserve_aspect_ratio,
is_virtual_placement,
window_width,
window_height,
);
instances.push(SixelInstance {
position: [x, y],
tex_coords,
size,
alpha,
_padding: 0.0,
});
}
}
if instances.is_empty() {
return Ok(());
}
log::debug!(
"[GRAPHICS] Rendering {} sixel graphics (from {} total graphics provided)",
instances.len(),
graphics.len()
);
let required_capacity = instances.len();
if required_capacity > self.instance_capacity {
let new_capacity = (required_capacity * 2).max(32);
self.instance_buffer = device.create_buffer(&BufferDescriptor {
label: Some("Sixel Instance Buffer"),
size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.instance_capacity = new_capacity;
}
queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
render_pass.set_pipeline(&self.pipeline);
render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
let mut instance_idx = 0u32;
for g in graphics {
if let Some(cached) = self.texture_cache.get(&g.id) {
render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
render_pass.draw(0..4, instance_idx..(instance_idx + 1));
instance_idx += 1;
}
}
Ok(())
}
pub fn render_for_pane(
&mut self,
device: &Device,
queue: &Queue,
render_pass: &mut RenderPass,
graphics: &[GraphicRenderInfo],
pane_geometry: PaneRenderGeometry,
) -> Result<(), RenderError> {
let PaneRenderGeometry {
window_width,
window_height,
pane_origin_x,
pane_origin_y,
} = pane_geometry;
if graphics.is_empty() {
return Ok(());
}
let mut instances = Vec::with_capacity(graphics.len());
for g in graphics {
let (
id,
row,
col,
_width_cells,
_height_cells,
alpha,
_scroll_offset_rows,
dest_off_x,
dest_off_y,
crop,
has_cols,
has_rows,
) = (
g.id,
g.screen_row,
g.col,
g.width_cells,
g.height_cells,
g.alpha,
g.scroll_offset_rows,
g.destination_offset_x,
g.destination_offset_y,
g.source_crop,
g.has_cols,
g.has_rows,
);
if let Some(cached) = self.texture_cache.get_mut(&id) {
cached.last_used = Instant::now();
let tex_info = &cached.texture;
let top_px = row as f32 * self.cell_height + dest_off_y as f32;
let clip_px = (-top_px).max(0.0);
let draw_y_px = top_px.max(0.0);
let x = (pane_origin_x + col as f32 * self.cell_width + dest_off_x as f32)
/ window_width;
let y = (pane_origin_y + draw_y_px) / window_height;
const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
let is_virtual_placement = id & VIRTUAL_PLACEMENT_ID_FLAG != 0;
let (tex_coords, size) = compute_graphic_geometry(
tex_info.width as f32,
tex_info.height as f32,
crop,
_width_cells,
_height_cells,
self.cell_width,
self.cell_height,
clip_px,
has_cols,
has_rows,
self.preserve_aspect_ratio,
is_virtual_placement,
window_width,
window_height,
);
instances.push(SixelInstance {
position: [x, y],
tex_coords,
size,
alpha,
_padding: 0.0,
});
}
}
if instances.is_empty() {
return Ok(());
}
let required_capacity = instances.len();
if required_capacity > self.instance_capacity {
let new_capacity = (required_capacity * 2).max(32);
self.instance_buffer = device.create_buffer(&BufferDescriptor {
label: Some("Sixel Instance Buffer"),
size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.instance_capacity = new_capacity;
}
queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
render_pass.set_pipeline(&self.pipeline);
render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
let mut instance_idx = 0u32;
for g in graphics {
if let Some(cached) = self.texture_cache.get(&g.id) {
render_pass.set_bind_group(0, &cached.texture.bind_group, &[]);
render_pass.draw(0..4, instance_idx..(instance_idx + 1));
instance_idx += 1;
}
}
Ok(())
}
pub fn remove_texture(&mut self, id: u64) {
self.texture_cache.remove(&id);
}
pub fn clear_cache(&mut self) {
self.texture_cache.clear();
}
pub fn cache_size(&self) -> usize {
self.texture_cache.len()
}
pub fn update_cell_dimensions(
&mut self,
cell_width: f32,
cell_height: f32,
window_padding: f32,
) {
self.cell_width = cell_width;
self.cell_height = cell_height;
self.window_padding = window_padding;
}
pub fn set_content_offset_y(&mut self, offset: f32) {
self.content_offset_y = offset;
}
pub fn set_content_offset_x(&mut self, offset: f32) {
self.content_offset_x = offset;
}
pub fn set_preserve_aspect_ratio(&mut self, preserve: bool) {
self.preserve_aspect_ratio = preserve;
}
pub fn update_scaling_mode(&mut self, device: &Device, scaling_mode: ImageScalingMode) {
self.sampler = gpu_utils::create_sampler_with_filter(
device,
scaling_mode.to_filter_mode(),
Some("Sixel Sampler"),
);
self.texture_cache.clear();
}
}