Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d43b024d2 | ||
|
|
67b9b11131 | ||
|
|
dd0fcbc671 | ||
|
|
c9c91b6aa2 | ||
|
|
4727e51d1b | ||
|
|
04b6776ebf |
+1
-1
@@ -87,7 +87,7 @@ flate2 = "1"
|
|||||||
|
|
||||||
# Video decoding for WebM support (video only, no audio)
|
# Video decoding for WebM support (video only, no audio)
|
||||||
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
||||||
ffmpeg-next = { version = "8.1", optional = true }
|
ffmpeg-next = { version = "9.0", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["webm"]
|
default = ["webm"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use std::time::Instant;
|
||||||
use zterm::terminal::Terminal;
|
use zterm::terminal::Terminal;
|
||||||
use zterm::vt_parser::Parser;
|
use zterm::vt_parser::Parser;
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
const ASCII_PRINTABLE: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
const ASCII_PRINTABLE: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
||||||
const CONTROL_CHARS: &[u8] = b"\n\t";
|
const CONTROL_CHARS: &[u8] = b"\n\t";
|
||||||
@@ -47,8 +47,14 @@ where
|
|||||||
let mb = total_size as f64 / 1024.0 / 1024.0;
|
let mb = total_size as f64 / 1024.0 / 1024.0;
|
||||||
let rate = mb / elapsed.as_secs_f64();
|
let rate = mb / elapsed.as_secs_f64();
|
||||||
|
|
||||||
println!(" {:<24} : {:>6.2}s @ {:.1} MB/s ({} reps, {:.2} MB each)",
|
println!(
|
||||||
name, elapsed.as_secs_f64(), rate, repetitions, data_size as f64 / 1024.0 / 1024.0);
|
" {:<24} : {:>6.2}s @ {:.1} MB/s ({} reps, {:.2} MB each)",
|
||||||
|
name,
|
||||||
|
elapsed.as_secs_f64(),
|
||||||
|
rate,
|
||||||
|
repetitions,
|
||||||
|
data_size as f64 / 1024.0 / 1024.0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -111,7 +117,8 @@ fn main() {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// 20%: reset + cursor + repeat + mode
|
// 20%: reset + cursor + repeat + mode
|
||||||
csi_data.extend_from_slice(b"\x1b[39m\x1b[10`a\x1b[100b\x1b[?1l");
|
csi_data
|
||||||
|
.extend_from_slice(b"\x1b[39m\x1b[10`a\x1b[100b\x1b[?1l");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +131,9 @@ fn main() {
|
|||||||
// Benchmark 3: Long escape codes (matches Kitty's long_escape_codes)
|
// Benchmark 3: Long escape codes (matches Kitty's long_escape_codes)
|
||||||
println!("\n--- Long escape codes ---");
|
println!("\n--- Long escape codes ---");
|
||||||
let mut long_esc_data = Vec::new();
|
let mut long_esc_data = Vec::new();
|
||||||
let long_content: String = (0..8024).map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char).collect();
|
let long_content: String = (0..8024)
|
||||||
|
.map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char)
|
||||||
|
.collect();
|
||||||
for _ in 0..1024 {
|
for _ in 0..1024 {
|
||||||
// OSC 6 - document reporting, ignored after parsing
|
// OSC 6 - document reporting, ignored after parsing
|
||||||
long_esc_data.extend_from_slice(b"\x1b]6;");
|
long_esc_data.extend_from_slice(b"\x1b]6;");
|
||||||
@@ -137,6 +146,8 @@ fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
println!("\n=== Benchmark Complete ===");
|
println!("\n=== Benchmark Complete ===");
|
||||||
println!("\nNote: These benchmarks include terminal state updates but NOT GPU rendering.");
|
println!(
|
||||||
|
"\nNote: These benchmarks include terminal state updates but NOT GPU rendering."
|
||||||
|
);
|
||||||
println!("Compare with: kitten __benchmark__ (without --render flag)");
|
println!("Compare with: kitten __benchmark__ (without --render flag)");
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-47
@@ -16,11 +16,15 @@ use std::path::PathBuf;
|
|||||||
/// Find a color font (emoji font) that contains the given character using fontconfig.
|
/// Find a color font (emoji font) that contains the given character using fontconfig.
|
||||||
/// Returns the path to the font file if found.
|
/// Returns the path to the font file if found.
|
||||||
pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
|
pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
|
||||||
use fontconfig_sys as fcsys;
|
|
||||||
use fcsys::*;
|
|
||||||
use fcsys::constants::{FC_CHARSET, FC_COLOR, FC_FILE};
|
use fcsys::constants::{FC_CHARSET, FC_COLOR, FC_FILE};
|
||||||
|
use fcsys::*;
|
||||||
|
use fontconfig_sys as fcsys;
|
||||||
|
|
||||||
log::debug!("find_color_font_for_char: looking for color font for U+{:04X} '{}'", c as u32, c);
|
log::debug!(
|
||||||
|
"find_color_font_for_char: looking for color font for U+{:04X} '{}'",
|
||||||
|
c as u32,
|
||||||
|
c
|
||||||
|
);
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Create a pattern
|
// Create a pattern
|
||||||
@@ -58,28 +62,54 @@ pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
|
|||||||
let font_path = if !matched.is_null() && result == FcResultMatch {
|
let font_path = if !matched.is_null() && result == FcResultMatch {
|
||||||
// Check if the matched font is actually a color font
|
// Check if the matched font is actually a color font
|
||||||
let mut is_color: i32 = 0;
|
let mut is_color: i32 = 0;
|
||||||
let has_color = FcPatternGetBool(matched, FC_COLOR.as_ptr() as *const i8, 0, &mut is_color) == FcResultMatch && is_color != 0;
|
let has_color = FcPatternGetBool(
|
||||||
|
matched,
|
||||||
|
FC_COLOR.as_ptr() as *const i8,
|
||||||
|
0,
|
||||||
|
&mut is_color,
|
||||||
|
) == FcResultMatch
|
||||||
|
&& is_color != 0;
|
||||||
|
|
||||||
log::debug!("find_color_font_for_char: matched font, is_color={}", has_color);
|
log::debug!(
|
||||||
|
"find_color_font_for_char: matched font, is_color={}",
|
||||||
|
has_color
|
||||||
|
);
|
||||||
|
|
||||||
if has_color {
|
if has_color {
|
||||||
// Get the file path from the matched pattern
|
// Get the file path from the matched pattern
|
||||||
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch {
|
if FcPatternGetString(
|
||||||
|
matched,
|
||||||
|
FC_FILE.as_ptr() as *const i8,
|
||||||
|
0,
|
||||||
|
&mut file_ptr,
|
||||||
|
) == FcResultMatch
|
||||||
|
{
|
||||||
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
||||||
let path = PathBuf::from(path_cstr.to_string_lossy().into_owned());
|
let path =
|
||||||
log::debug!("find_color_font_for_char: found color font {:?}", path);
|
PathBuf::from(path_cstr.to_string_lossy().into_owned());
|
||||||
|
log::debug!(
|
||||||
|
"find_color_font_for_char: found color font {:?}",
|
||||||
|
path
|
||||||
|
);
|
||||||
Some(path)
|
Some(path)
|
||||||
} else {
|
} else {
|
||||||
log::debug!("find_color_font_for_char: couldn't get file path");
|
log::debug!(
|
||||||
|
"find_color_font_for_char: couldn't get file path"
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::debug!("find_color_font_for_char: matched font is not a color font");
|
log::debug!(
|
||||||
|
"find_color_font_for_char: matched font is not a color font"
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::debug!("find_color_font_for_char: no match found (result={:?})", result);
|
log::debug!(
|
||||||
|
"find_color_font_for_char: no match found (result={:?})",
|
||||||
|
result
|
||||||
|
);
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,11 +161,16 @@ impl ColorFontRenderer {
|
|||||||
// Create Cairo font face from FreeType face
|
// Create Cairo font face from FreeType face
|
||||||
match cairo::FontFace::create_from_ft(&ft_face) {
|
match cairo::FontFace::create_from_ft(&ft_face) {
|
||||||
Ok(cairo_face) => {
|
Ok(cairo_face) => {
|
||||||
self.faces.insert(path.clone(), (ft_face, cairo_face));
|
self.faces
|
||||||
|
.insert(path.clone(), (ft_face, cairo_face));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to create Cairo font face for {:?}: {:?}", path, e);
|
log::warn!(
|
||||||
|
"Failed to create Cairo font face for {:?}: {:?}",
|
||||||
|
path,
|
||||||
|
e
|
||||||
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,14 +195,22 @@ impl ColorFontRenderer {
|
|||||||
cell_width: u32,
|
cell_width: u32,
|
||||||
cell_height: u32,
|
cell_height: u32,
|
||||||
) -> Option<(u32, u32, Vec<u8>, f32, f32)> {
|
) -> Option<(u32, u32, Vec<u8>, f32, f32)> {
|
||||||
log::debug!("render_color_glyph: U+{:04X} '{}' font={:?}", c as u32, c, font_path);
|
log::debug!(
|
||||||
|
"render_color_glyph: U+{:04X} '{}' font={:?}",
|
||||||
|
c as u32,
|
||||||
|
c,
|
||||||
|
font_path
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure faces are loaded
|
// Ensure faces are loaded
|
||||||
if !self.ensure_faces_loaded(font_path) {
|
if !self.ensure_faces_loaded(font_path) {
|
||||||
log::debug!("render_color_glyph: failed to load faces");
|
log::debug!("render_color_glyph: failed to load faces");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
log::debug!("render_color_glyph: faces loaded successfully, faces count={}", self.faces.len());
|
log::debug!(
|
||||||
|
"render_color_glyph: faces loaded successfully, faces count={}",
|
||||||
|
self.faces.len()
|
||||||
|
);
|
||||||
|
|
||||||
// Get glyph index from FreeType face
|
// Get glyph index from FreeType face
|
||||||
// Note: We do NOT call set_pixel_sizes here because CBDT (bitmap) fonts have fixed sizes
|
// Note: We do NOT call set_pixel_sizes here because CBDT (bitmap) fonts have fixed sizes
|
||||||
@@ -175,15 +218,26 @@ impl ColorFontRenderer {
|
|||||||
let glyph_index = {
|
let glyph_index = {
|
||||||
let face_entry = self.faces.get(font_path);
|
let face_entry = self.faces.get(font_path);
|
||||||
if face_entry.is_none() {
|
if face_entry.is_none() {
|
||||||
log::debug!("render_color_glyph: face not found in hashmap after ensure_faces_loaded!");
|
log::debug!(
|
||||||
|
"render_color_glyph: face not found in hashmap after ensure_faces_loaded!"
|
||||||
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (ft_face, _) = face_entry?;
|
let (ft_face, _) = face_entry?;
|
||||||
log::debug!("render_color_glyph: got ft_face, getting char index for U+{:04X}", c as u32);
|
log::debug!(
|
||||||
|
"render_color_glyph: got ft_face, getting char index for U+{:04X}",
|
||||||
|
c as u32
|
||||||
|
);
|
||||||
let idx = ft_face.get_char_index(c as usize);
|
let idx = ft_face.get_char_index(c as usize);
|
||||||
log::debug!("render_color_glyph: FreeType glyph index for U+{:04X} = {:?}", c as u32, idx);
|
log::debug!(
|
||||||
|
"render_color_glyph: FreeType glyph index for U+{:04X} = {:?}",
|
||||||
|
c as u32,
|
||||||
|
idx
|
||||||
|
);
|
||||||
if idx.is_none() {
|
if idx.is_none() {
|
||||||
log::debug!("render_color_glyph: glyph index is None - char not in font!");
|
log::debug!(
|
||||||
|
"render_color_glyph: glyph index is None - char not in font!"
|
||||||
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
idx?
|
idx?
|
||||||
@@ -199,18 +253,29 @@ impl ColorFontRenderer {
|
|||||||
let render_width = (cell_width * 2).max(cell_height) as i32;
|
let render_width = (cell_width * 2).max(cell_height) as i32;
|
||||||
let render_height = cell_height as i32;
|
let render_height = cell_height as i32;
|
||||||
|
|
||||||
log::debug!("render_color_glyph: render size {}x{}", render_width, render_height);
|
log::debug!(
|
||||||
|
"render_color_glyph: render size {}x{}",
|
||||||
|
render_width,
|
||||||
|
render_height
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure we have a large enough surface
|
// Ensure we have a large enough surface
|
||||||
let surface_width = render_width.max(256);
|
let surface_width = render_width.max(256);
|
||||||
let surface_height = render_height.max(256);
|
let surface_height = render_height.max(256);
|
||||||
|
|
||||||
if self.surface.is_none() || self.surface_size.0 < surface_width || self.surface_size.1 < surface_height {
|
if self.surface.is_none()
|
||||||
|
|| self.surface_size.0 < surface_width
|
||||||
|
|| self.surface_size.1 < surface_height
|
||||||
|
{
|
||||||
let new_width = surface_width.max(self.surface_size.0);
|
let new_width = surface_width.max(self.surface_size.0);
|
||||||
let new_height = surface_height.max(self.surface_size.1);
|
let new_height = surface_height.max(self.surface_size.1);
|
||||||
match ImageSurface::create(Format::ARgb32, new_width, new_height) {
|
match ImageSurface::create(Format::ARgb32, new_width, new_height) {
|
||||||
Ok(surface) => {
|
Ok(surface) => {
|
||||||
log::debug!("render_color_glyph: created Cairo surface {}x{}", new_width, new_height);
|
log::debug!(
|
||||||
|
"render_color_glyph: created Cairo surface {}x{}",
|
||||||
|
new_width,
|
||||||
|
new_height
|
||||||
|
);
|
||||||
self.surface = Some(surface);
|
self.surface = Some(surface);
|
||||||
self.surface_size = (new_width, new_height);
|
self.surface_size = (new_width, new_height);
|
||||||
}
|
}
|
||||||
@@ -253,8 +318,12 @@ impl ColorFontRenderer {
|
|||||||
let mut glyph = cairo::Glyph::new(glyph_index as u64, 0.0, 0.0);
|
let mut glyph = cairo::Glyph::new(glyph_index as u64, 0.0, 0.0);
|
||||||
let mut text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
let mut text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||||
|
|
||||||
while current_size > min_size && (text_extents.width() > target_width || text_extents.height() > target_height) {
|
while current_size > min_size
|
||||||
let ratio = (target_width / text_extents.width()).min(target_height / text_extents.height());
|
&& (text_extents.width() > target_width
|
||||||
|
|| text_extents.height() > target_height)
|
||||||
|
{
|
||||||
|
let ratio = (target_width / text_extents.width())
|
||||||
|
.min(target_height / text_extents.height());
|
||||||
let new_size = (ratio * current_size).max(min_size);
|
let new_size = (ratio * current_size).max(min_size);
|
||||||
if new_size >= current_size {
|
if new_size >= current_size {
|
||||||
current_size -= 2.0;
|
current_size -= 2.0;
|
||||||
@@ -265,24 +334,38 @@ impl ColorFontRenderer {
|
|||||||
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("render_color_glyph: fitted font size {:.1} (from {:.1}), glyph extents {:.1}x{:.1}",
|
log::debug!(
|
||||||
current_size, font_size_px, text_extents.width(), text_extents.height());
|
"render_color_glyph: fitted font size {:.1} (from {:.1}), glyph extents {:.1}x{:.1}",
|
||||||
|
current_size,
|
||||||
|
font_size_px,
|
||||||
|
text_extents.width(),
|
||||||
|
text_extents.height()
|
||||||
|
);
|
||||||
|
|
||||||
// Get font metrics for positioning with the final size
|
// Get font metrics for positioning with the final size
|
||||||
let font_extents = cr.font_extents().ok()?;
|
let font_extents = cr.font_extents().ok()?;
|
||||||
log::debug!("render_color_glyph: font extents - ascent={:.1}, descent={:.1}, height={:.1}",
|
log::debug!(
|
||||||
font_extents.ascent(), font_extents.descent(), font_extents.height());
|
"render_color_glyph: font extents - ascent={:.1}, descent={:.1}, height={:.1}",
|
||||||
|
font_extents.ascent(),
|
||||||
|
font_extents.descent(),
|
||||||
|
font_extents.height()
|
||||||
|
);
|
||||||
|
|
||||||
// Create glyph with positioning at baseline
|
// Create glyph with positioning at baseline
|
||||||
// y position should be at baseline (ascent from top)
|
// y position should be at baseline (ascent from top)
|
||||||
glyph = cairo::Glyph::new(glyph_index as u64, 0.0, font_extents.ascent());
|
glyph =
|
||||||
|
cairo::Glyph::new(glyph_index as u64, 0.0, font_extents.ascent());
|
||||||
|
|
||||||
// Get final glyph extents for sizing
|
// Get final glyph extents for sizing
|
||||||
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||||
log::debug!("render_color_glyph: text extents - width={:.1}, height={:.1}, x_bearing={:.1}, y_bearing={:.1}, x_advance={:.1}",
|
log::debug!(
|
||||||
text_extents.width(), text_extents.height(),
|
"render_color_glyph: text extents - width={:.1}, height={:.1}, x_bearing={:.1}, y_bearing={:.1}, x_advance={:.1}",
|
||||||
text_extents.x_bearing(), text_extents.y_bearing(),
|
text_extents.width(),
|
||||||
text_extents.x_advance());
|
text_extents.height(),
|
||||||
|
text_extents.x_bearing(),
|
||||||
|
text_extents.y_bearing(),
|
||||||
|
text_extents.x_advance()
|
||||||
|
);
|
||||||
|
|
||||||
// Set source color to white - the atlas stores colors directly for emoji
|
// Set source color to white - the atlas stores colors directly for emoji
|
||||||
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||||
@@ -303,7 +386,11 @@ impl ColorFontRenderer {
|
|||||||
let glyph_width = text_extents.width().ceil() as u32;
|
let glyph_width = text_extents.width().ceil() as u32;
|
||||||
let glyph_height = text_extents.height().ceil() as u32;
|
let glyph_height = text_extents.height().ceil() as u32;
|
||||||
|
|
||||||
log::debug!("render_color_glyph: glyph size {}x{}", glyph_width, glyph_height);
|
log::debug!(
|
||||||
|
"render_color_glyph: glyph size {}x{}",
|
||||||
|
glyph_width,
|
||||||
|
glyph_height
|
||||||
|
);
|
||||||
|
|
||||||
if glyph_width == 0 || glyph_height == 0 {
|
if glyph_width == 0 || glyph_height == 0 {
|
||||||
log::debug!("render_color_glyph: zero size glyph, returning None");
|
log::debug!("render_color_glyph: zero size glyph, returning None");
|
||||||
@@ -318,7 +405,11 @@ impl ColorFontRenderer {
|
|||||||
let src_x = x_offset.max(0.0) as i32;
|
let src_x = x_offset.max(0.0) as i32;
|
||||||
let src_y = (font_extents.ascent() + y_offset).max(0.0) as i32;
|
let src_y = (font_extents.ascent() + y_offset).max(0.0) as i32;
|
||||||
|
|
||||||
log::debug!("render_color_glyph: source rect starts at ({}, {})", src_x, src_y);
|
log::debug!(
|
||||||
|
"render_color_glyph: source rect starts at ({}, {})",
|
||||||
|
src_x,
|
||||||
|
src_y
|
||||||
|
);
|
||||||
|
|
||||||
// Get surface data
|
// Get surface data
|
||||||
let stride = surface.stride() as usize;
|
let stride = surface.stride() as usize;
|
||||||
@@ -337,10 +428,15 @@ impl ColorFontRenderer {
|
|||||||
let src_pixel_x = src_x + x;
|
let src_pixel_x = src_x + x;
|
||||||
let src_pixel_y = src_y + y;
|
let src_pixel_y = src_y + y;
|
||||||
|
|
||||||
if src_pixel_x >= 0 && src_pixel_x < self.surface_size.0
|
if src_pixel_x >= 0
|
||||||
&& src_pixel_y >= 0 && src_pixel_y < self.surface_size.1 {
|
&& src_pixel_x < self.surface_size.0
|
||||||
let src_idx = (src_pixel_y as usize) * stride + (src_pixel_x as usize) * 4;
|
&& src_pixel_y >= 0
|
||||||
let dst_idx = (y as usize * out_width as usize + x as usize) * 4;
|
&& src_pixel_y < self.surface_size.1
|
||||||
|
{
|
||||||
|
let src_idx = (src_pixel_y as usize) * stride
|
||||||
|
+ (src_pixel_x as usize) * 4;
|
||||||
|
let dst_idx =
|
||||||
|
(y as usize * out_width as usize + x as usize) * 4;
|
||||||
|
|
||||||
if src_idx + 3 < surface_data.len() {
|
if src_idx + 3 < surface_data.len() {
|
||||||
// Cairo uses ARGB in native byte order (on little-endian: BGRA in memory)
|
// Cairo uses ARGB in native byte order (on little-endian: BGRA in memory)
|
||||||
@@ -361,9 +457,12 @@ impl ColorFontRenderer {
|
|||||||
// Un-premultiply alpha if needed (Cairo uses premultiplied alpha)
|
// Un-premultiply alpha if needed (Cairo uses premultiplied alpha)
|
||||||
if a > 0 && a < 255 {
|
if a > 0 && a < 255 {
|
||||||
let inv_alpha = 255.0 / a as f32;
|
let inv_alpha = 255.0 / a as f32;
|
||||||
rgba[dst_idx] = (r as f32 * inv_alpha).min(255.0) as u8;
|
rgba[dst_idx] =
|
||||||
rgba[dst_idx + 1] = (g as f32 * inv_alpha).min(255.0) as u8;
|
(r as f32 * inv_alpha).min(255.0) as u8;
|
||||||
rgba[dst_idx + 2] = (b as f32 * inv_alpha).min(255.0) as u8;
|
rgba[dst_idx + 1] =
|
||||||
|
(g as f32 * inv_alpha).min(255.0) as u8;
|
||||||
|
rgba[dst_idx + 2] =
|
||||||
|
(b as f32 * inv_alpha).min(255.0) as u8;
|
||||||
rgba[dst_idx + 3] = a;
|
rgba[dst_idx + 3] = a;
|
||||||
} else {
|
} else {
|
||||||
rgba[dst_idx] = r;
|
rgba[dst_idx] = r;
|
||||||
@@ -376,13 +475,20 @@ impl ColorFontRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("render_color_glyph: extracted {}x{} pixels, {} non-zero, has_color={}",
|
log::debug!(
|
||||||
out_width, out_height, non_zero_pixels, has_color);
|
"render_color_glyph: extracted {}x{} pixels, {} non-zero, has_color={}",
|
||||||
|
out_width,
|
||||||
|
out_height,
|
||||||
|
non_zero_pixels,
|
||||||
|
has_color
|
||||||
|
);
|
||||||
|
|
||||||
// Check if we actually got any non-transparent pixels
|
// Check if we actually got any non-transparent pixels
|
||||||
let has_content = rgba.chunks(4).any(|p| p[3] > 0);
|
let has_content = rgba.chunks(4).any(|p| p[3] > 0);
|
||||||
if !has_content {
|
if !has_content {
|
||||||
log::debug!("render_color_glyph: no visible content, returning None");
|
log::debug!(
|
||||||
|
"render_color_glyph: no visible content, returning None"
|
||||||
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,8 +496,13 @@ impl ColorFontRenderer {
|
|||||||
let offset_x = text_extents.x_bearing() as f32;
|
let offset_x = text_extents.x_bearing() as f32;
|
||||||
let offset_y = -text_extents.y_bearing() as f32;
|
let offset_y = -text_extents.y_bearing() as f32;
|
||||||
|
|
||||||
log::debug!("render_color_glyph: SUCCESS - returning {}x{} glyph, offset=({:.1}, {:.1})",
|
log::debug!(
|
||||||
out_width, out_height, offset_x, offset_y);
|
"render_color_glyph: SUCCESS - returning {}x{} glyph, offset=({:.1}, {:.1})",
|
||||||
|
out_width,
|
||||||
|
out_height,
|
||||||
|
offset_x,
|
||||||
|
offset_y
|
||||||
|
);
|
||||||
|
|
||||||
Some((out_width, out_height, rgba, offset_x, offset_y))
|
Some((out_width, out_height, rgba, offset_x, offset_y))
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -8,7 +8,9 @@ use std::fs;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Position of the tab bar.
|
/// Position of the tab bar.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default,
|
||||||
|
)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum TabBarPosition {
|
pub enum TabBarPosition {
|
||||||
/// Tab bar at the top of the window.
|
/// Tab bar at the top of the window.
|
||||||
@@ -81,7 +83,7 @@ impl Keybind {
|
|||||||
"shift" => shift = true,
|
"shift" => shift = true,
|
||||||
"super" | "meta" | "cmd" => super_key = true,
|
"super" | "meta" | "cmd" => super_key = true,
|
||||||
"" => {} // Empty parts from splitting
|
"" => {} // Empty parts from splitting
|
||||||
_ => {} // Unknown modifiers ignored
|
_ => {} // Unknown modifiers ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +285,9 @@ impl Default for Keybindings {
|
|||||||
|
|
||||||
impl Keybindings {
|
impl Keybindings {
|
||||||
/// Builds a lookup map from parsed keybinds to actions.
|
/// Builds a lookup map from parsed keybinds to actions.
|
||||||
pub fn build_action_map(&self) -> HashMap<(bool, bool, bool, bool, String), Action> {
|
pub fn build_action_map(
|
||||||
|
&self,
|
||||||
|
) -> HashMap<(bool, bool, bool, bool, String), Action> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
let bindings: &[(&Keybind, Action)] = &[
|
let bindings: &[(&Keybind, Action)] = &[
|
||||||
@@ -396,9 +400,7 @@ impl Config {
|
|||||||
|
|
||||||
match fs::read_to_string(&config_path) {
|
match fs::read_to_string(&config_path) {
|
||||||
Ok(contents) => match serde_json::from_str(&contents) {
|
Ok(contents) => match serde_json::from_str(&contents) {
|
||||||
Ok(config) => {
|
Ok(config) => config,
|
||||||
config
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to parse config file: {}", e);
|
log::error!("Failed to parse config file: {}", e);
|
||||||
Self::default()
|
Self::default()
|
||||||
@@ -425,8 +427,9 @@ impl Config {
|
|||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(self)
|
let json = serde_json::to_string_pretty(self).map_err(|e| {
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
||||||
|
})?;
|
||||||
|
|
||||||
fs::write(&config_path, json)?;
|
fs::write(&config_path, json)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+7
-1
@@ -30,7 +30,13 @@ impl EdgeGlow {
|
|||||||
pub const DURATION_MS: u64 = 500;
|
pub const DURATION_MS: u64 = 500;
|
||||||
|
|
||||||
/// Create a new edge glow animation constrained to a pane's bounds.
|
/// Create a new edge glow animation constrained to a pane's bounds.
|
||||||
pub fn new(direction: Direction, pane_x: f32, pane_y: f32, pane_width: f32, pane_height: f32) -> Self {
|
pub fn new(
|
||||||
|
direction: Direction,
|
||||||
|
pane_x: f32,
|
||||||
|
pane_y: f32,
|
||||||
|
pane_width: f32,
|
||||||
|
pane_height: f32,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
direction,
|
direction,
|
||||||
start_time: std::time::Instant::now(),
|
start_time: std::time::Instant::now(),
|
||||||
|
|||||||
+52
-25
@@ -57,8 +57,8 @@ impl FontVariant {
|
|||||||
/// Note: For emoji, use `find_color_font_for_char` from the color_font module instead,
|
/// Note: For emoji, use `find_color_font_for_char` from the color_font module instead,
|
||||||
/// which explicitly requests color fonts.
|
/// which explicitly requests color fonts.
|
||||||
pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
||||||
use fontconfig_sys as fcsys;
|
|
||||||
use fcsys::*;
|
use fcsys::*;
|
||||||
|
use fontconfig_sys as fcsys;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Create a pattern
|
// Create a pattern
|
||||||
@@ -93,7 +93,12 @@ pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
|||||||
// Get the file path from the matched pattern
|
// Get the file path from the matched pattern
|
||||||
let mut file_ptr: *mut FcChar8 = std::ptr::null_mut();
|
let mut file_ptr: *mut FcChar8 = std::ptr::null_mut();
|
||||||
let fc_file_cstr = CStr::from_bytes_with_nul(b"file\0").unwrap();
|
let fc_file_cstr = CStr::from_bytes_with_nul(b"file\0").unwrap();
|
||||||
if FcPatternGetString(matched, fc_file_cstr.as_ptr(), 0, &mut file_ptr) == FcResultMatch
|
if FcPatternGetString(
|
||||||
|
matched,
|
||||||
|
fc_file_cstr.as_ptr(),
|
||||||
|
0,
|
||||||
|
&mut file_ptr,
|
||||||
|
) == FcResultMatch
|
||||||
{
|
{
|
||||||
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
||||||
Some(PathBuf::from(path_cstr.to_string_lossy().into_owned()))
|
Some(PathBuf::from(path_cstr.to_string_lossy().into_owned()))
|
||||||
@@ -119,9 +124,9 @@ pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
|||||||
/// Returns paths for (regular, bold, italic, bold_italic).
|
/// Returns paths for (regular, bold, italic, bold_italic).
|
||||||
/// Any variant that can't be found will be None.
|
/// Any variant that can't be found will be None.
|
||||||
pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
||||||
use fontconfig_sys as fcsys;
|
use fcsys::constants::{FC_FAMILY, FC_FILE, FC_SLANT, FC_WEIGHT};
|
||||||
use fcsys::*;
|
use fcsys::*;
|
||||||
use fcsys::constants::{FC_FAMILY, FC_WEIGHT, FC_SLANT, FC_FILE};
|
use fontconfig_sys as fcsys;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
|
||||||
let mut results: [Option<PathBuf>; 4] = [None, None, None, None];
|
let mut results: [Option<PathBuf>; 4] = [None, None, None, None];
|
||||||
@@ -149,7 +154,11 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set family name
|
// Set family name
|
||||||
FcPatternAddString(pat, FC_FAMILY.as_ptr() as *const i8, family_cstr.as_ptr() as *const u8);
|
FcPatternAddString(
|
||||||
|
pat,
|
||||||
|
FC_FAMILY.as_ptr() as *const i8,
|
||||||
|
family_cstr.as_ptr() as *const u8,
|
||||||
|
);
|
||||||
// Set weight
|
// Set weight
|
||||||
FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight);
|
FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight);
|
||||||
// Set slant
|
// Set slant
|
||||||
@@ -163,9 +172,16 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
|||||||
|
|
||||||
if result == FcResultMatch && !matched.is_null() {
|
if result == FcResultMatch && !matched.is_null() {
|
||||||
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch {
|
if FcPatternGetString(
|
||||||
|
matched,
|
||||||
|
FC_FILE.as_ptr() as *const i8,
|
||||||
|
0,
|
||||||
|
&mut file_ptr,
|
||||||
|
) == FcResultMatch
|
||||||
|
{
|
||||||
if !file_ptr.is_null() {
|
if !file_ptr.is_null() {
|
||||||
let path_cstr = std::ffi::CStr::from_ptr(file_ptr as *const i8);
|
let path_cstr =
|
||||||
|
std::ffi::CStr::from_ptr(file_ptr as *const i8);
|
||||||
if let Ok(path_str) = path_cstr.to_str() {
|
if let Ok(path_str) = path_cstr.to_str() {
|
||||||
results[idx] = Some(PathBuf::from(path_str));
|
results[idx] = Some(PathBuf::from(path_str));
|
||||||
}
|
}
|
||||||
@@ -210,7 +226,9 @@ pub fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
|
|||||||
/// Load font variants for a font family.
|
/// Load font variants for a font family.
|
||||||
/// Returns array of font variants, with index 0 being the regular font.
|
/// Returns array of font variants, with index 0 being the regular font.
|
||||||
/// Falls back to hardcoded paths if fontconfig fails.
|
/// Falls back to hardcoded paths if fontconfig fails.
|
||||||
pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'static>, [Option<FontVariant>; 4]) {
|
pub fn load_font_family(
|
||||||
|
font_family: Option<&str>,
|
||||||
|
) -> (Box<[u8]>, FontRef<'static>, [Option<FontVariant>; 4]) {
|
||||||
// Try to use fontconfig to find the font family
|
// Try to use fontconfig to find the font family
|
||||||
if let Some(family) = font_family {
|
if let Some(family) = font_family {
|
||||||
let paths = find_font_family_variants(family);
|
let paths = find_font_family_variants(family);
|
||||||
@@ -232,23 +250,32 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
|
|||||||
return (font_data, primary_font, variants);
|
return (font_data, primary_font, variants);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::warn!("Failed to load font family '{}', falling back to defaults", family);
|
log::warn!(
|
||||||
|
"Failed to load font family '{}', falling back to defaults",
|
||||||
|
family
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: try hardcoded paths
|
// Fallback: try hardcoded paths
|
||||||
let fallback_fonts = [
|
let fallback_fonts = [
|
||||||
("/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf",
|
(
|
||||||
"/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf",
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf",
|
||||||
"/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf",
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf",
|
||||||
"/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf"),
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf",
|
||||||
("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf",
|
"/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf",
|
||||||
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf",
|
),
|
||||||
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Italic.ttf",
|
(
|
||||||
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-BoldItalic.ttf"),
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf",
|
||||||
("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf",
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf",
|
||||||
"/usr/share/fonts/TTF/JetBrainsMono-Bold.ttf",
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Italic.ttf",
|
||||||
"/usr/share/fonts/TTF/JetBrainsMono-Italic.ttf",
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-BoldItalic.ttf",
|
||||||
"/usr/share/fonts/TTF/JetBrainsMono-BoldItalic.ttf"),
|
),
|
||||||
|
(
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-BoldItalic.ttf",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (regular, bold, italic, bold_italic) in fallback_fonts {
|
for (regular, bold, italic, bold_italic) in fallback_fonts {
|
||||||
@@ -264,18 +291,18 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
|
|||||||
load_font_variant(std::path::Path::new(bold_italic)),
|
load_font_variant(std::path::Path::new(bold_italic)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (font_data, primary_font, variants);
|
return (font_data, primary_font, variants);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: try NotoSansMono
|
// Last resort: try NotoSansMono
|
||||||
let noto_regular = std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf");
|
let noto_regular =
|
||||||
|
std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf");
|
||||||
if let Some(regular_variant) = load_font_variant(noto_regular) {
|
if let Some(regular_variant) = load_font_variant(noto_regular) {
|
||||||
let primary_font = regular_variant.clone_font();
|
let primary_font = regular_variant.clone_font();
|
||||||
let font_data = regular_variant.clone_data();
|
let font_data = regular_variant.clone_data();
|
||||||
let variants: [Option<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
let variants: [Option<FontVariant>; 4] =
|
||||||
|
[Some(regular_variant), None, None, None];
|
||||||
|
|
||||||
return (font_data, primary_font, variants);
|
return (font_data, primary_font, variants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ struct GridParams {
|
|||||||
selection_start_row: i32,
|
selection_start_row: i32,
|
||||||
selection_end_col: i32,
|
selection_end_col: i32,
|
||||||
selection_end_row: i32,
|
selection_end_row: i32,
|
||||||
|
selection_row_max_col: array<i32, 256>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GPUCell instance data (matches Rust GPUCell struct)
|
// GPUCell instance data (matches Rust GPUCell struct)
|
||||||
@@ -187,7 +188,7 @@ struct SpriteInfo {
|
|||||||
var<uniform> color_table: ColorTable;
|
var<uniform> color_table: ColorTable;
|
||||||
|
|
||||||
@group(1) @binding(1)
|
@group(1) @binding(1)
|
||||||
var<uniform> grid_params: GridParams;
|
var<storage, read> grid_params: GridParams;
|
||||||
|
|
||||||
@group(1) @binding(2)
|
@group(1) @binding(2)
|
||||||
var<storage, read> cells: array<GPUCell>;
|
var<storage, read> cells: array<GPUCell>;
|
||||||
@@ -278,6 +279,11 @@ fn is_cell_selected(col: u32, row: u32) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only highlight cells that have content in them or to their right on this row
|
||||||
|
if grid_params.selection_row_max_col[row] < 0 || col > u32(grid_params.selection_row_max_col[row]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let sel_start_col = u32(grid_params.selection_start_col);
|
let sel_start_col = u32(grid_params.selection_start_col);
|
||||||
let sel_start_row = u32(grid_params.selection_start_row);
|
let sel_start_row = u32(grid_params.selection_start_row);
|
||||||
let sel_end_col = u32(grid_params.selection_end_col);
|
let sel_end_col = u32(grid_params.selection_end_col);
|
||||||
|
|||||||
+26
-10
@@ -4,6 +4,20 @@
|
|||||||
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
|
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
|
||||||
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
/// Unique identifier for a pane.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct PaneId(pub u64);
|
||||||
|
|
||||||
|
impl PaneId {
|
||||||
|
/// Generate a new unique pane ID.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
static COUNTER: std::sync::atomic::AtomicU64 =
|
||||||
|
std::sync::atomic::AtomicU64::new(0);
|
||||||
|
Self(COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// CONSTANTS
|
// CONSTANTS
|
||||||
@@ -43,19 +57,19 @@ pub const COLORED_GLYPH_FLAG: u32 = 0x80000000;
|
|||||||
/// Pre-rendered cursor sprite indices (like Kitty's cursor_shape_map).
|
/// Pre-rendered cursor sprite indices (like Kitty's cursor_shape_map).
|
||||||
/// These sprites are created at fixed indices in the sprite array after initialization.
|
/// These sprites are created at fixed indices in the sprite array after initialization.
|
||||||
/// Index 0 is reserved for "no glyph" (empty cell).
|
/// Index 0 is reserved for "no glyph" (empty cell).
|
||||||
pub const CURSOR_SPRITE_BEAM: u32 = 1; // Bar/beam cursor (vertical line on left)
|
pub const CURSOR_SPRITE_BEAM: u32 = 1; // Bar/beam cursor (vertical line on left)
|
||||||
pub const CURSOR_SPRITE_UNDERLINE: u32 = 2; // Underline cursor (horizontal line at bottom)
|
pub const CURSOR_SPRITE_UNDERLINE: u32 = 2; // Underline cursor (horizontal line at bottom)
|
||||||
pub const CURSOR_SPRITE_HOLLOW: u32 = 3; // Hollow/unfocused cursor (outline rectangle)
|
pub const CURSOR_SPRITE_HOLLOW: u32 = 3; // Hollow/unfocused cursor (outline rectangle)
|
||||||
|
|
||||||
/// Pre-rendered decoration sprite indices (like Kitty's decoration sprites).
|
/// Pre-rendered decoration sprite indices (like Kitty's decoration sprites).
|
||||||
/// These are created after cursor sprites and used for text decorations.
|
/// These are created after cursor sprites and used for text decorations.
|
||||||
/// The shader uses these to render underlines, strikethrough, etc.
|
/// The shader uses these to render underlines, strikethrough, etc.
|
||||||
pub const DECORATION_SPRITE_STRIKETHROUGH: u32 = 4; // Strikethrough line
|
pub const DECORATION_SPRITE_STRIKETHROUGH: u32 = 4; // Strikethrough line
|
||||||
pub const DECORATION_SPRITE_UNDERLINE: u32 = 5; // Single underline
|
pub const DECORATION_SPRITE_UNDERLINE: u32 = 5; // Single underline
|
||||||
pub const DECORATION_SPRITE_DOUBLE_UNDERLINE: u32 = 6; // Double underline
|
pub const DECORATION_SPRITE_DOUBLE_UNDERLINE: u32 = 6; // Double underline
|
||||||
pub const DECORATION_SPRITE_UNDERCURL: u32 = 7; // Wavy/curly underline
|
pub const DECORATION_SPRITE_UNDERCURL: u32 = 7; // Wavy/curly underline
|
||||||
pub const DECORATION_SPRITE_DOTTED: u32 = 8; // Dotted underline
|
pub const DECORATION_SPRITE_DOTTED: u32 = 8; // Dotted underline
|
||||||
pub const DECORATION_SPRITE_DASHED: u32 = 9; // Dashed underline
|
pub const DECORATION_SPRITE_DASHED: u32 = 9; // Dashed underline
|
||||||
|
|
||||||
/// First available sprite index for regular glyphs (after reserved cursor and decoration sprites)
|
/// First available sprite index for regular glyphs (after reserved cursor and decoration sprites)
|
||||||
pub const FIRST_GLYPH_SPRITE: u32 = 10;
|
pub const FIRST_GLYPH_SPRITE: u32 = 10;
|
||||||
@@ -84,7 +98,8 @@ impl GlyphVertex {
|
|||||||
|
|
||||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||||
wgpu::VertexBufferLayout {
|
wgpu::VertexBufferLayout {
|
||||||
array_stride: std::mem::size_of::<GlyphVertex>() as wgpu::BufferAddress,
|
array_stride: std::mem::size_of::<GlyphVertex>()
|
||||||
|
as wgpu::BufferAddress,
|
||||||
step_mode: wgpu::VertexStepMode::Vertex,
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
attributes: &Self::ATTRIBS,
|
attributes: &Self::ATTRIBS,
|
||||||
}
|
}
|
||||||
@@ -148,8 +163,8 @@ pub struct ImageUniforms {
|
|||||||
pub src_y: f32,
|
pub src_y: f32,
|
||||||
pub src_width: f32,
|
pub src_width: f32,
|
||||||
pub src_height: f32,
|
pub src_height: f32,
|
||||||
|
pub dim_factor: f32,
|
||||||
pub _padding1: f32,
|
pub _padding1: f32,
|
||||||
pub _padding2: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -225,7 +240,7 @@ pub struct FontCellMetrics {
|
|||||||
/// works in pure NDC space without needing pixel offsets.
|
/// works in pure NDC space without needing pixel offsets.
|
||||||
/// Cell dimensions are integers like Kitty for pixel-perfect rendering.
|
/// Cell dimensions are integers like Kitty for pixel-perfect rendering.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
pub struct GridParams {
|
pub struct GridParams {
|
||||||
pub cols: u32,
|
pub cols: u32,
|
||||||
pub rows: u32,
|
pub rows: u32,
|
||||||
@@ -240,6 +255,7 @@ pub struct GridParams {
|
|||||||
pub selection_start_row: i32,
|
pub selection_start_row: i32,
|
||||||
pub selection_end_col: i32,
|
pub selection_end_col: i32,
|
||||||
pub selection_end_row: i32,
|
pub selection_end_row: i32,
|
||||||
|
pub selection_row_max_col: [i32; 256],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GPU quad instance for instanced rectangle rendering.
|
/// GPU quad instance for instanced rectangle rendering.
|
||||||
|
|||||||
+294
-90
@@ -11,7 +11,7 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use flate2::read::ZlibDecoder;
|
use flate2::read::ZlibDecoder;
|
||||||
use image::{codecs::gif::GifDecoder, AnimationDecoder, ImageFormat};
|
use image::{AnimationDecoder, ImageFormat, codecs::gif::GifDecoder};
|
||||||
|
|
||||||
/// Action to perform with the graphics command.
|
/// Action to perform with the graphics command.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
@@ -137,10 +137,18 @@ pub struct GraphicsCommand {
|
|||||||
pub delete_target: DeleteTarget,
|
pub delete_target: DeleteTarget,
|
||||||
/// Unicode placeholder (virtual placement).
|
/// Unicode placeholder (virtual placement).
|
||||||
pub unicode_placeholder: bool,
|
pub unicode_placeholder: bool,
|
||||||
|
/// Parent image ID (for relative placement).
|
||||||
|
pub parent_image_id: Option<u32>,
|
||||||
|
/// Parent placement ID (for relative placement).
|
||||||
|
pub parent_placement_id: Option<u32>,
|
||||||
|
/// Horizontal cell displacement from parent.
|
||||||
|
pub h_offset: i32,
|
||||||
|
/// Vertical cell displacement from parent.
|
||||||
|
pub v_offset: i32,
|
||||||
/// Parent image ID (for animation frames).
|
/// Parent image ID (for animation frames).
|
||||||
pub parent_id: Option<u32>,
|
pub parent_id: Option<u32>,
|
||||||
/// Parent placement ID (for animation frames).
|
/// Parent placement ID (for animation frames).
|
||||||
pub parent_placement_id: Option<u32>,
|
pub parent_placement_id_anim: Option<u32>,
|
||||||
/// Frame number (for animation).
|
/// Frame number (for animation).
|
||||||
pub frame_number: Option<u32>,
|
pub frame_number: Option<u32>,
|
||||||
/// Frame gap in milliseconds (z key for animation frames).
|
/// Frame gap in milliseconds (z key for animation frames).
|
||||||
@@ -213,8 +221,10 @@ impl GraphicsCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_animation =
|
let is_animation = matches!(
|
||||||
matches!(cmd.action, Action::AnimationFrame | Action::AnimationControl);
|
cmd.action,
|
||||||
|
Action::AnimationFrame | Action::AnimationControl
|
||||||
|
);
|
||||||
|
|
||||||
// Second pass: parse all keys with correct interpretation
|
// Second pass: parse all keys with correct interpretation
|
||||||
for (key, value) in pairs {
|
for (key, value) in pairs {
|
||||||
@@ -256,6 +266,17 @@ impl GraphicsCommand {
|
|||||||
cmd.height = value.parse().ok();
|
cmd.height = value.parse().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"P" => {
|
||||||
|
if is_animation {
|
||||||
|
// P = parent_frame_index for animation
|
||||||
|
cmd.base_frame = value.parse().ok();
|
||||||
|
} else {
|
||||||
|
cmd.parent_image_id = value.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Q" => cmd.parent_placement_id = value.parse().ok(),
|
||||||
|
"H" => cmd.h_offset = value.parse().unwrap_or(0),
|
||||||
|
"V" => cmd.v_offset = value.parse().unwrap_or(0),
|
||||||
"x" => cmd.src_x = value.parse().unwrap_or(0),
|
"x" => cmd.src_x = value.parse().unwrap_or(0),
|
||||||
"y" => cmd.src_y = value.parse().unwrap_or(0),
|
"y" => cmd.src_y = value.parse().unwrap_or(0),
|
||||||
"w" => cmd.src_width = value.parse().unwrap_or(0),
|
"w" => cmd.src_width = value.parse().unwrap_or(0),
|
||||||
@@ -336,7 +357,11 @@ impl GraphicsCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decode base64 payload
|
// Decode base64 payload
|
||||||
log::debug!("Parsing payload: len={}, content={:?}", payload_part.len(), std::str::from_utf8(payload_part).ok());
|
log::debug!(
|
||||||
|
"Parsing payload: len={}, content={:?}",
|
||||||
|
payload_part.len(),
|
||||||
|
std::str::from_utf8(payload_part).ok()
|
||||||
|
);
|
||||||
if !payload_part.is_empty() {
|
if !payload_part.is_empty() {
|
||||||
if let Ok(payload_str) = std::str::from_utf8(payload_part) {
|
if let Ok(payload_str) = std::str::from_utf8(payload_part) {
|
||||||
if let Ok(decoded) = base64_decode(payload_str) {
|
if let Ok(decoded) = base64_decode(payload_str) {
|
||||||
@@ -436,8 +461,13 @@ pub fn decode_gif(
|
|||||||
return Err(GraphicsError::GifDecodeFailed);
|
return Err(GraphicsError::GifDecodeFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Decoded GIF: {}x{}, {} frames, {}ms total duration",
|
log::debug!(
|
||||||
width, height, frames.len(), total_duration_ms);
|
"Decoded GIF: {}x{}, {} frames, {}ms total duration",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames.len(),
|
||||||
|
total_duration_ms
|
||||||
|
);
|
||||||
|
|
||||||
let first_frame = frames[0].data.clone();
|
let first_frame = frames[0].data.clone();
|
||||||
|
|
||||||
@@ -465,7 +495,7 @@ pub fn decode_gif(
|
|||||||
pub fn decode_webm(
|
pub fn decode_webm(
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<(u32, u32, Vec<u8>, Option<AnimationData>), GraphicsError> {
|
) -> Result<(u32, u32, Vec<u8>, Option<AnimationData>), GraphicsError> {
|
||||||
use ffmpeg::format::{input, Pixel};
|
use ffmpeg::format::{Pixel, input};
|
||||||
use ffmpeg::media::Type;
|
use ffmpeg::media::Type;
|
||||||
use ffmpeg::software::scaling::{
|
use ffmpeg::software::scaling::{
|
||||||
context::Context as ScalingContext, flag::Flags,
|
context::Context as ScalingContext, flag::Flags,
|
||||||
@@ -804,7 +834,9 @@ pub struct ImageStorage {
|
|||||||
current_chunked_id: Option<u32>,
|
current_chunked_id: Option<u32>,
|
||||||
/// Next auto-generated image ID.
|
/// Next auto-generated image ID.
|
||||||
next_id: u32,
|
next_id: u32,
|
||||||
/// Flag indicating images have changed and need re-upload to GPU.
|
/// Images that have been updated and need re-upload to GPU.
|
||||||
|
pub dirty_images: std::collections::HashSet<u32>,
|
||||||
|
/// Flag indicating placements have changed and need re-render.
|
||||||
pub dirty: bool,
|
pub dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,6 +856,7 @@ impl ImageStorage {
|
|||||||
chunk_buffer: HashMap::new(),
|
chunk_buffer: HashMap::new(),
|
||||||
current_chunked_id: None,
|
current_chunked_id: None,
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
|
dirty_images: std::collections::HashSet::new(),
|
||||||
dirty: false,
|
dirty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -947,8 +980,15 @@ impl ImageStorage {
|
|||||||
cell_width,
|
cell_width,
|
||||||
cell_height,
|
cell_height,
|
||||||
);
|
);
|
||||||
log::debug!("Placed image id={} at col={} row={}, cols={} rows={}, placements={}",
|
log::debug!(
|
||||||
id, cursor_col, cursor_row, cols, rows, self.placements.len());
|
"Placed image id={} at col={} row={}, cols={} rows={}, placements={}",
|
||||||
|
id,
|
||||||
|
cursor_col,
|
||||||
|
cursor_row,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
self.placements.len()
|
||||||
|
);
|
||||||
Some(PlacementResult {
|
Some(PlacementResult {
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
@@ -979,6 +1019,7 @@ impl ImageStorage {
|
|||||||
let virtual_placement = cmd.unicode_placeholder;
|
let virtual_placement = cmd.unicode_placeholder;
|
||||||
|
|
||||||
if self.images.contains_key(&id) {
|
if self.images.contains_key(&id) {
|
||||||
|
log::debug!("Put image {}: found in storage", id);
|
||||||
let (cols, rows) = self.place_image(
|
let (cols, rows) = self.place_image(
|
||||||
cmd,
|
cmd,
|
||||||
cursor_col,
|
cursor_col,
|
||||||
@@ -994,6 +1035,11 @@ impl ImageStorage {
|
|||||||
};
|
};
|
||||||
(self.format_response(cmd, Ok(id)), Some(placement_result))
|
(self.format_response(cmd, Ok(id)), Some(placement_result))
|
||||||
} else {
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Put image {}: NOT found in storage! (storage size: {})",
|
||||||
|
id,
|
||||||
|
self.images.len()
|
||||||
|
);
|
||||||
(
|
(
|
||||||
self.format_response(cmd, Err(GraphicsError::ImageNotFound)),
|
self.format_response(cmd, Err(GraphicsError::ImageNotFound)),
|
||||||
None,
|
None,
|
||||||
@@ -1003,37 +1049,47 @@ impl ImageStorage {
|
|||||||
|
|
||||||
/// Handle a delete command.
|
/// Handle a delete command.
|
||||||
fn handle_delete(&mut self, cmd: &GraphicsCommand) {
|
fn handle_delete(&mut self, cmd: &GraphicsCommand) {
|
||||||
|
log::debug!(
|
||||||
|
"Delete command: target={:?}, id={:?}",
|
||||||
|
cmd.delete_target,
|
||||||
|
cmd.image_id
|
||||||
|
);
|
||||||
match &cmd.delete_target {
|
match &cmd.delete_target {
|
||||||
DeleteTarget::All => {
|
DeleteTarget::All => {
|
||||||
|
log::debug!("Deleting all images and placements");
|
||||||
self.images.clear();
|
self.images.clear();
|
||||||
self.placements.clear();
|
self.placements.clear();
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
DeleteTarget::ById(id) => {
|
DeleteTarget::ById(id) => {
|
||||||
let id = cmd.image_id.unwrap_or(*id);
|
let id = cmd.image_id.unwrap_or(*id);
|
||||||
self.images.remove(&id);
|
log::debug!("Removing all placements of image by id={}", id);
|
||||||
self.placements.retain(|p| p.image_id != id);
|
self.placements.retain(|p| p.image_id != id);
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
DeleteTarget::AtCursor => {
|
DeleteTarget::AtCursor => {
|
||||||
// Would need cursor position - simplified for now
|
log::debug!("Deleting placements at cursor");
|
||||||
self.placements.clear();
|
self.placements.clear();
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Other delete modes not yet implemented
|
log::debug!("Unhandled delete target: {:?}", cmd.delete_target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle an animation frame command (a=f).
|
/// Handle an animation frame command (a=f).
|
||||||
/// This adds a frame to an existing image's animation.
|
/// This adds a frame to an existing image's animation.
|
||||||
fn handle_animation_frame(&mut self, mut cmd: GraphicsCommand) -> Option<String> {
|
fn handle_animation_frame(
|
||||||
|
&mut self,
|
||||||
|
mut cmd: GraphicsCommand,
|
||||||
|
) -> Option<String> {
|
||||||
let id = match cmd.image_id {
|
let id = match cmd.image_id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
log::warn!("AnimationFrame without image_id");
|
log::warn!("AnimationFrame without image_id");
|
||||||
return self.format_response(&cmd, Err(GraphicsError::MissingId));
|
return self
|
||||||
|
.format_response(&cmd, Err(GraphicsError::MissingId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1056,15 +1112,25 @@ impl ImageStorage {
|
|||||||
Ok(p) => p.trim().to_string(),
|
Ok(p) => p.trim().to_string(),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::warn!("Invalid file path in animation frame");
|
log::warn!("Invalid file path in animation frame");
|
||||||
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed));
|
return self.format_response(
|
||||||
|
&cmd,
|
||||||
|
Err(GraphicsError::FileReadFailed),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log::debug!("Reading animation frame from file: {}", path);
|
log::debug!("Reading animation frame from file: {}", path);
|
||||||
match std::fs::read(&path) {
|
match std::fs::read(&path) {
|
||||||
Ok(data) => cmd.payload = data,
|
Ok(data) => cmd.payload = data,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to read animation frame file {}: {}", path, e);
|
log::warn!(
|
||||||
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed));
|
"Failed to read animation frame file {}: {}",
|
||||||
|
path,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return self.format_response(
|
||||||
|
&cmd,
|
||||||
|
Err(GraphicsError::FileReadFailed),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete temp file after reading
|
// Delete temp file after reading
|
||||||
@@ -1076,20 +1142,38 @@ impl ImageStorage {
|
|||||||
let shm_name = match std::str::from_utf8(&cmd.payload) {
|
let shm_name = match std::str::from_utf8(&cmd.payload) {
|
||||||
Ok(p) => p.trim().to_string(),
|
Ok(p) => p.trim().to_string(),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::warn!("Invalid shared memory name in animation frame");
|
log::warn!(
|
||||||
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed));
|
"Invalid shared memory name in animation frame"
|
||||||
|
);
|
||||||
|
return self.format_response(
|
||||||
|
&cmd,
|
||||||
|
Err(GraphicsError::FileReadFailed),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let shm_path = format!("/dev/shm/{}", shm_name);
|
let shm_path = format!("/dev/shm/{}", shm_name);
|
||||||
log::debug!("Reading animation frame from shared memory: {}", shm_path);
|
log::debug!(
|
||||||
|
"Reading animation frame from shared memory: {}",
|
||||||
|
shm_path
|
||||||
|
);
|
||||||
match std::fs::read(&shm_path) {
|
match std::fs::read(&shm_path) {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
log::debug!("Read {} bytes from shared memory", data.len());
|
log::debug!(
|
||||||
|
"Read {} bytes from shared memory",
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
cmd.payload = data;
|
cmd.payload = data;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to read animation frame shm {}: {}", shm_path, e);
|
log::warn!(
|
||||||
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed));
|
"Failed to read animation frame shm {}: {}",
|
||||||
|
shm_path,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return self.format_response(
|
||||||
|
&cmd,
|
||||||
|
Err(GraphicsError::FileReadFailed),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remove shared memory object after reading
|
// Remove shared memory object after reading
|
||||||
@@ -1125,7 +1209,10 @@ impl ImageStorage {
|
|||||||
Format::Gif => {
|
Format::Gif => {
|
||||||
// Unlikely, but handle it
|
// Unlikely, but handle it
|
||||||
log::warn!("GIF format in animation frame - not supported");
|
log::warn!("GIF format in animation frame - not supported");
|
||||||
return self.format_response(&cmd, Err(GraphicsError::UnsupportedFormat));
|
return self.format_response(
|
||||||
|
&cmd,
|
||||||
|
Err(GraphicsError::UnsupportedFormat),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1134,7 +1221,8 @@ impl ImageStorage {
|
|||||||
Some(img) => img,
|
Some(img) => img,
|
||||||
None => {
|
None => {
|
||||||
log::warn!("AnimationFrame for non-existent image {}", id);
|
log::warn!("AnimationFrame for non-existent image {}", id);
|
||||||
return self.format_response(&cmd, Err(GraphicsError::ImageNotFound));
|
return self
|
||||||
|
.format_response(&cmd, Err(GraphicsError::ImageNotFound));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1145,7 +1233,11 @@ impl ImageStorage {
|
|||||||
// This MUST happen before compositing so that frame 0 exists for c=1
|
// This MUST happen before compositing so that frame 0 exists for c=1
|
||||||
if image.animation.is_none() {
|
if image.animation.is_none() {
|
||||||
// Debug: check base image alpha values
|
// Debug: check base image alpha values
|
||||||
let transparent_count = image.data.chunks(4).filter(|p| p.len() == 4 && p[3] < 255).count();
|
let transparent_count = image
|
||||||
|
.data
|
||||||
|
.chunks(4)
|
||||||
|
.filter(|p| p.len() == 4 && p[3] < 255)
|
||||||
|
.count();
|
||||||
let total_pixels = image.data.len() / 4;
|
let total_pixels = image.data.len() / 4;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Creating animation base frame: {}/{} pixels have alpha < 255, data len = {}",
|
"Creating animation base frame: {}/{} pixels have alpha < 255, data len = {}",
|
||||||
@@ -1185,7 +1277,9 @@ impl ImageStorage {
|
|||||||
if base_idx < anim.frames.len() {
|
if base_idx < anim.frames.len() {
|
||||||
let base_data = &anim.frames[base_idx].data;
|
let base_data = &anim.frames[base_idx].data;
|
||||||
|
|
||||||
if frame_data.len() == expected_size && base_data.len() == expected_size {
|
if frame_data.len() == expected_size
|
||||||
|
&& base_data.len() == expected_size
|
||||||
|
{
|
||||||
// Both frames are full size - composite them
|
// Both frames are full size - composite them
|
||||||
// composition_mode: 0 = alpha blend, 1 = overwrite
|
// composition_mode: 0 = alpha blend, 1 = overwrite
|
||||||
let mut composited = base_data.clone();
|
let mut composited = base_data.clone();
|
||||||
@@ -1220,17 +1314,28 @@ impl ImageStorage {
|
|||||||
|
|
||||||
// Standard alpha compositing: out = src + dst * (1 - src_a)
|
// Standard alpha compositing: out = src + dst * (1 - src_a)
|
||||||
let inv_a = 255 - src_a32;
|
let inv_a = 255 - src_a32;
|
||||||
composited[i] = ((src_r * src_a32 + dst_r * inv_a) / 255) as u8;
|
composited[i] =
|
||||||
composited[i + 1] = ((src_g * src_a32 + dst_g * inv_a) / 255) as u8;
|
((src_r * src_a32 + dst_r * inv_a) / 255)
|
||||||
composited[i + 2] = ((src_b * src_a32 + dst_b * inv_a) / 255) as u8;
|
as u8;
|
||||||
composited[i + 3] = (src_a32 + dst_a * inv_a / 255).min(255) as u8;
|
composited[i + 1] =
|
||||||
|
((src_g * src_a32 + dst_g * inv_a) / 255)
|
||||||
|
as u8;
|
||||||
|
composited[i + 2] =
|
||||||
|
((src_b * src_a32 + dst_b * inv_a) / 255)
|
||||||
|
as u8;
|
||||||
|
composited[i + 3] =
|
||||||
|
(src_a32 + dst_a * inv_a / 255).min(255)
|
||||||
|
as u8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// else: src_a == 0, keep base pixel (already in composited)
|
// else: src_a == 0, keep base pixel (already in composited)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: check alpha values
|
// Debug: check alpha values
|
||||||
let transparent_count = composited.chunks(4).filter(|p| p.len() == 4 && p[3] < 255).count();
|
let transparent_count = composited
|
||||||
|
.chunks(4)
|
||||||
|
.filter(|p| p.len() == 4 && p[3] < 255)
|
||||||
|
.count();
|
||||||
let total_pixels = composited.len() / 4;
|
let total_pixels = composited.len() / 4;
|
||||||
if transparent_count > 0 {
|
if transparent_count > 0 {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -1241,7 +1346,9 @@ impl ImageStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composited
|
composited
|
||||||
} else if frame_data.len() < expected_size && base_data.len() == expected_size {
|
} else if frame_data.len() < expected_size
|
||||||
|
&& base_data.len() == expected_size
|
||||||
|
{
|
||||||
// Partial frame data - just use base for now
|
// Partial frame data - just use base for now
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Frame data size {} < expected {}, using base frame {}",
|
"Frame data size {} < expected {}, using base frame {}",
|
||||||
@@ -1258,7 +1365,10 @@ impl ImageStorage {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Base frame doesn't exist yet (shouldn't happen now), pad the data
|
// Base frame doesn't exist yet (shouldn't happen now), pad the data
|
||||||
log::warn!("Base frame {} doesn't exist, padding data", base_frame_num);
|
log::warn!(
|
||||||
|
"Base frame {} doesn't exist, padding data",
|
||||||
|
base_frame_num
|
||||||
|
);
|
||||||
let mut data = frame_data;
|
let mut data = frame_data;
|
||||||
data.resize(expected_size, 0);
|
data.resize(expected_size, 0);
|
||||||
data
|
data
|
||||||
@@ -1321,12 +1431,16 @@ impl ImageStorage {
|
|||||||
|
|
||||||
/// Handle an animation control command (a=a).
|
/// Handle an animation control command (a=a).
|
||||||
/// This controls playback of an animated image.
|
/// This controls playback of an animated image.
|
||||||
fn handle_animation_control(&mut self, cmd: &GraphicsCommand) -> Option<String> {
|
fn handle_animation_control(
|
||||||
|
&mut self,
|
||||||
|
cmd: &GraphicsCommand,
|
||||||
|
) -> Option<String> {
|
||||||
let id = match cmd.image_id {
|
let id = match cmd.image_id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
log::warn!("AnimationControl without image_id");
|
log::warn!("AnimationControl without image_id");
|
||||||
return self.format_response(cmd, Err(GraphicsError::MissingId));
|
return self
|
||||||
|
.format_response(cmd, Err(GraphicsError::MissingId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1342,7 +1456,8 @@ impl ImageStorage {
|
|||||||
Some(img) => img,
|
Some(img) => img,
|
||||||
None => {
|
None => {
|
||||||
log::warn!("AnimationControl for non-existent image {}", id);
|
log::warn!("AnimationControl for non-existent image {}", id);
|
||||||
return self.format_response(cmd, Err(GraphicsError::ImageNotFound));
|
return self
|
||||||
|
.format_response(cmd, Err(GraphicsError::ImageNotFound));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1359,7 +1474,11 @@ impl ImageStorage {
|
|||||||
AnimationState::Loading
|
AnimationState::Loading
|
||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
log::debug!("Animation {} running ({} frames)", id, anim.frames.len());
|
log::debug!(
|
||||||
|
"Animation {} running ({} frames)",
|
||||||
|
id,
|
||||||
|
anim.frames.len()
|
||||||
|
);
|
||||||
// Reset frame start when starting animation
|
// Reset frame start when starting animation
|
||||||
anim.frame_start = None;
|
anim.frame_start = None;
|
||||||
anim.looping = true;
|
anim.looping = true;
|
||||||
@@ -1375,7 +1494,11 @@ impl ImageStorage {
|
|||||||
anim.current_frame = frame_num as usize - 1; // 1-indexed to 0-indexed
|
anim.current_frame = frame_num as usize - 1; // 1-indexed to 0-indexed
|
||||||
// No need to clone - renderer uses current_frame_data()
|
// No need to clone - renderer uses current_frame_data()
|
||||||
anim.frame_start = None; // Reset timing
|
anim.frame_start = None; // Reset timing
|
||||||
log::debug!("Animation {} jumped to frame {}", id, frame_num);
|
log::debug!(
|
||||||
|
"Animation {} jumped to frame {}",
|
||||||
|
id,
|
||||||
|
frame_num
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1388,7 +1511,11 @@ impl ImageStorage {
|
|||||||
anim.looping = true;
|
anim.looping = true;
|
||||||
anim.loops_remaining = Some(loop_count);
|
anim.loops_remaining = Some(loop_count);
|
||||||
}
|
}
|
||||||
log::debug!("Animation {} loop count set to {:?}", id, anim.loops_remaining);
|
log::debug!(
|
||||||
|
"Animation {} loop count set to {:?}",
|
||||||
|
id,
|
||||||
|
anim.loops_remaining
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
@@ -1451,7 +1578,9 @@ impl ImageStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete temp file after reading
|
// Delete temp file after reading
|
||||||
if cmd.transmission == Transmission::TempFile && file_path.is_none() {
|
if cmd.transmission == Transmission::TempFile
|
||||||
|
&& file_path.is_none()
|
||||||
|
{
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1485,7 +1614,9 @@ impl ImageStorage {
|
|||||||
// Payload is already the data
|
// Payload is already the data
|
||||||
// Try to detect format from magic bytes if format is default
|
// Try to detect format from magic bytes if format is default
|
||||||
if cmd.format == Format::Rgba && cmd.payload.len() >= 6 {
|
if cmd.format == Format::Rgba && cmd.payload.len() >= 6 {
|
||||||
if &cmd.payload[0..6] == b"GIF89a" || &cmd.payload[0..6] == b"GIF87a" {
|
if &cmd.payload[0..6] == b"GIF89a"
|
||||||
|
|| &cmd.payload[0..6] == b"GIF87a"
|
||||||
|
{
|
||||||
cmd.format = Format::Gif;
|
cmd.format = Format::Gif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1516,26 +1647,36 @@ impl ImageStorage {
|
|||||||
(w, h, d, None)
|
(w, h, d, None)
|
||||||
}
|
}
|
||||||
Format::Rgba => {
|
Format::Rgba => {
|
||||||
let w = cmd.width.ok_or(GraphicsError::MissingDimensions)?;
|
let w =
|
||||||
let h = cmd.height.ok_or(GraphicsError::MissingDimensions)?;
|
cmd.width.ok_or(GraphicsError::MissingDimensions)?;
|
||||||
|
let h =
|
||||||
|
cmd.height.ok_or(GraphicsError::MissingDimensions)?;
|
||||||
let expected_size = (w * h * 4) as usize;
|
let expected_size = (w * h * 4) as usize;
|
||||||
if cmd.payload.len() != expected_size {
|
if cmd.payload.len() != expected_size {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"RGBA image size mismatch: declared {}x{} ({} bytes expected), got {} bytes",
|
"RGBA image size mismatch: declared {}x{} ({} bytes expected), got {} bytes",
|
||||||
w, h, expected_size, cmd.payload.len()
|
w,
|
||||||
|
h,
|
||||||
|
expected_size,
|
||||||
|
cmd.payload.len()
|
||||||
);
|
);
|
||||||
return Err(GraphicsError::InvalidData);
|
return Err(GraphicsError::InvalidData);
|
||||||
}
|
}
|
||||||
(w, h, cmd.payload.clone(), None)
|
(w, h, cmd.payload.clone(), None)
|
||||||
}
|
}
|
||||||
Format::Rgb => {
|
Format::Rgb => {
|
||||||
let w = cmd.width.ok_or(GraphicsError::MissingDimensions)?;
|
let w =
|
||||||
let h = cmd.height.ok_or(GraphicsError::MissingDimensions)?;
|
cmd.width.ok_or(GraphicsError::MissingDimensions)?;
|
||||||
|
let h =
|
||||||
|
cmd.height.ok_or(GraphicsError::MissingDimensions)?;
|
||||||
let expected_size = (w * h * 3) as usize;
|
let expected_size = (w * h * 3) as usize;
|
||||||
if cmd.payload.len() != expected_size {
|
if cmd.payload.len() != expected_size {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"RGB image size mismatch: declared {}x{} ({} bytes expected), got {} bytes",
|
"RGB image size mismatch: declared {}x{} ({} bytes expected), got {} bytes",
|
||||||
w, h, expected_size, cmd.payload.len()
|
w,
|
||||||
|
h,
|
||||||
|
expected_size,
|
||||||
|
cmd.payload.len()
|
||||||
);
|
);
|
||||||
return Err(GraphicsError::InvalidData);
|
return Err(GraphicsError::InvalidData);
|
||||||
}
|
}
|
||||||
@@ -1562,6 +1703,7 @@ impl ImageStorage {
|
|||||||
animation,
|
animation,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.dirty_images.insert(id);
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
@@ -1611,6 +1753,24 @@ impl ImageStorage {
|
|||||||
cmd.rows as usize
|
cmd.rows as usize
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle relative positioning
|
||||||
|
let (final_col, final_row) = if let Some(p_id) = cmd.parent_image_id {
|
||||||
|
let q_id = cmd.parent_placement_id.unwrap_or(0);
|
||||||
|
if let Some(parent) = self
|
||||||
|
.placements
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.image_id == p_id && p.placement_id == q_id)
|
||||||
|
{
|
||||||
|
let col = parent.col as i32 + cmd.h_offset;
|
||||||
|
let row = parent.row as i32 + cmd.v_offset;
|
||||||
|
(col.max(0) as usize, row.max(0) as usize)
|
||||||
|
} else {
|
||||||
|
(cursor_col, cursor_row)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(cursor_col, cursor_row)
|
||||||
|
};
|
||||||
|
|
||||||
// Don't create actual placement for virtual placements (U=1)
|
// Don't create actual placement for virtual placements (U=1)
|
||||||
// Virtual placements are referenced by Unicode placeholders
|
// Virtual placements are referenced by Unicode placeholders
|
||||||
if cmd.unicode_placeholder {
|
if cmd.unicode_placeholder {
|
||||||
@@ -1626,8 +1786,8 @@ impl ImageStorage {
|
|||||||
let placement = ImagePlacement {
|
let placement = ImagePlacement {
|
||||||
image_id: id,
|
image_id: id,
|
||||||
placement_id: cmd.placement_id.unwrap_or(0),
|
placement_id: cmd.placement_id.unwrap_or(0),
|
||||||
col: cursor_col,
|
col: final_col,
|
||||||
row: cursor_row,
|
row: final_row,
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
z_index: cmd.z_index,
|
z_index: cmd.z_index,
|
||||||
@@ -1639,12 +1799,9 @@ impl ImageStorage {
|
|||||||
y_offset: cmd.y_offset,
|
y_offset: cmd.y_offset,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove existing placement with same ID if present
|
let pid = cmd.placement_id.unwrap_or(0);
|
||||||
if cmd.placement_id.is_some() {
|
self.placements
|
||||||
self.placements.retain(|p| {
|
.retain(|p| p.image_id != id || p.placement_id != pid);
|
||||||
p.image_id != id || p.placement_id != placement.placement_id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.placements.push(placement);
|
self.placements.push(placement);
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
@@ -1716,6 +1873,24 @@ impl ImageStorage {
|
|||||||
&self.placements
|
&self.placements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shift all image placements by a delta (e.g., when scrollback buffer wraps).
|
||||||
|
pub fn shift_placements(&mut self, delta: isize) {
|
||||||
|
if delta == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.placements.retain_mut(|p| {
|
||||||
|
let new_row = p.row as isize + delta;
|
||||||
|
if new_row < 0 {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
p.row = new_row as usize;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get an image by ID.
|
/// Get an image by ID.
|
||||||
pub fn get_image(&self, id: u32) -> Option<&ImageData> {
|
pub fn get_image(&self, id: u32) -> Option<&ImageData> {
|
||||||
self.images.get(&id)
|
self.images.get(&id)
|
||||||
@@ -1724,6 +1899,7 @@ impl ImageStorage {
|
|||||||
/// Clear the dirty flag.
|
/// Clear the dirty flag.
|
||||||
pub fn clear_dirty(&mut self) {
|
pub fn clear_dirty(&mut self) {
|
||||||
self.dirty = false;
|
self.dirty = false;
|
||||||
|
self.dirty_images.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update animations and return list of image IDs that changed frames.
|
/// Update animations and return list of image IDs that changed frames.
|
||||||
@@ -1742,13 +1918,19 @@ impl ImageStorage {
|
|||||||
// Initialize frame start time if not set
|
// Initialize frame start time if not set
|
||||||
if anim.frame_start.is_none() {
|
if anim.frame_start.is_none() {
|
||||||
anim.frame_start = Some(now);
|
anim.frame_start = Some(now);
|
||||||
log::debug!("Animation {} started, {} frames, first frame {}ms",
|
log::debug!(
|
||||||
id, anim.frames.len(), anim.frames[0].duration_ms);
|
"Animation {} started, {} frames, first frame {}ms",
|
||||||
|
id,
|
||||||
|
anim.frames.len(),
|
||||||
|
anim.frames[0].duration_ms
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let frame_start = anim.frame_start.unwrap();
|
let frame_start = anim.frame_start.unwrap();
|
||||||
let elapsed = now.duration_since(frame_start).as_millis() as u32;
|
let elapsed =
|
||||||
let current_frame_duration = anim.frames[anim.current_frame].duration_ms;
|
now.duration_since(frame_start).as_millis() as u32;
|
||||||
|
let current_frame_duration =
|
||||||
|
anim.frames[anim.current_frame].duration_ms;
|
||||||
|
|
||||||
if elapsed >= current_frame_duration {
|
if elapsed >= current_frame_duration {
|
||||||
// Advance to next frame
|
// Advance to next frame
|
||||||
@@ -1758,36 +1940,56 @@ impl ImageStorage {
|
|||||||
if anim.looping {
|
if anim.looping {
|
||||||
// Check loop count
|
// Check loop count
|
||||||
if let Some(ref mut loops) = anim.loops_remaining {
|
if let Some(ref mut loops) = anim.loops_remaining {
|
||||||
if *loops > 0 {
|
if *loops > 0 {
|
||||||
log::debug!("Animation {} looping, {} loops remaining", id, *loops - 1);
|
log::debug!(
|
||||||
*loops -= 1;
|
"Animation {} looping, {} loops remaining",
|
||||||
anim.current_frame = 0;
|
id,
|
||||||
} else {
|
*loops - 1
|
||||||
log::debug!("Animation {} stopped: no more loops", id);
|
);
|
||||||
// No more loops, stop
|
*loops -= 1;
|
||||||
anim.state = AnimationState::Stopped;
|
anim.current_frame = 0;
|
||||||
continue;
|
} else {
|
||||||
}
|
log::debug!(
|
||||||
|
"Animation {} stopped: no more loops",
|
||||||
} else {
|
id
|
||||||
log::debug!("Animation {} looping indefinitely", id);
|
);
|
||||||
// Infinite looping
|
// No more loops, stop
|
||||||
anim.current_frame = 0;
|
anim.state = AnimationState::Stopped;
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
}
|
} else {
|
||||||
log::debug!("Animation {} reached end, looping={}", id, anim.looping);
|
log::debug!(
|
||||||
if !anim.looping {
|
"Animation {} looping indefinitely",
|
||||||
log::debug!("Animation {} stopping (looping=false)", id);
|
id
|
||||||
}
|
);
|
||||||
// else: stay on last frame
|
// Infinite looping
|
||||||
} else {
|
anim.current_frame = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!(
|
||||||
|
"Animation {} reached end, looping={}",
|
||||||
|
id,
|
||||||
|
anim.looping
|
||||||
|
);
|
||||||
|
if !anim.looping {
|
||||||
|
log::debug!(
|
||||||
|
"Animation {} stopping (looping=false)",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// else: stay on last frame
|
||||||
|
} else {
|
||||||
anim.current_frame = next_frame;
|
anim.current_frame = next_frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Animation {} frame {} -> {} (elapsed {}ms >= {}ms)",
|
log::debug!(
|
||||||
id, old_frame, anim.current_frame, elapsed, current_frame_duration);
|
"Animation {} frame {} -> {} (elapsed {}ms >= {}ms)",
|
||||||
|
id,
|
||||||
|
old_frame,
|
||||||
|
anim.current_frame,
|
||||||
|
elapsed,
|
||||||
|
current_frame_duration
|
||||||
|
);
|
||||||
|
|
||||||
// Just update frame index - no data clone needed!
|
// Just update frame index - no data clone needed!
|
||||||
// The renderer will use current_frame_data() to get the right frame.
|
// The renderer will use current_frame_data() to get the right frame.
|
||||||
@@ -1809,7 +2011,9 @@ impl ImageStorage {
|
|||||||
self.images.values().any(|img| {
|
self.images.values().any(|img| {
|
||||||
img.animation
|
img.animation
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|a| a.state == AnimationState::Running && a.frames.len() > 1)
|
.map(|a| {
|
||||||
|
a.state == AnimationState::Running && a.frames.len() > 1
|
||||||
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+234
-93
@@ -3,9 +3,9 @@
|
|||||||
//! This module handles GPU-accelerated rendering of images in the terminal,
|
//! This module handles GPU-accelerated rendering of images in the terminal,
|
||||||
//! supporting the Kitty Graphics Protocol for inline image display.
|
//! supporting the Kitty Graphics Protocol for inline image display.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use crate::gpu_types::{ImageUniforms, PaneId};
|
||||||
use crate::gpu_types::ImageUniforms;
|
|
||||||
use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// GPU IMAGE
|
// GPU IMAGE
|
||||||
@@ -15,7 +15,6 @@ use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
|||||||
pub struct GpuImage {
|
pub struct GpuImage {
|
||||||
pub texture: wgpu::Texture,
|
pub texture: wgpu::Texture,
|
||||||
pub view: wgpu::TextureView,
|
pub view: wgpu::TextureView,
|
||||||
pub uniform_buffer: wgpu::Buffer,
|
|
||||||
pub bind_group: wgpu::BindGroup,
|
pub bind_group: wgpu::BindGroup,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
@@ -28,12 +27,20 @@ pub struct GpuImage {
|
|||||||
/// Manages GPU resources for image rendering.
|
/// Manages GPU resources for image rendering.
|
||||||
/// Handles uploading, caching, and preparing images for rendering.
|
/// Handles uploading, caching, and preparing images for rendering.
|
||||||
pub struct ImageRenderer {
|
pub struct ImageRenderer {
|
||||||
/// Bind group layout for image rendering.
|
/// Bind group layout for uniforms.
|
||||||
bind_group_layout: wgpu::BindGroupLayout,
|
uniform_layout: wgpu::BindGroupLayout,
|
||||||
|
/// Bind group layout for textures.
|
||||||
|
texture_layout: wgpu::BindGroupLayout,
|
||||||
/// Sampler for image textures.
|
/// Sampler for image textures.
|
||||||
sampler: wgpu::Sampler,
|
sampler: wgpu::Sampler,
|
||||||
/// Cached GPU textures for images, keyed by image ID.
|
/// Cached GPU textures for images, keyed by (pane_id, image_id).
|
||||||
textures: HashMap<u32, GpuImage>,
|
textures: HashMap<(PaneId, u32), GpuImage>,
|
||||||
|
/// Global uniform buffer for image renders.
|
||||||
|
pub uniform_buffer: wgpu::Buffer,
|
||||||
|
/// Bind group for image uniforms.
|
||||||
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
|
/// Minimum offset alignment for uniform buffers.
|
||||||
|
pub alignment: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageRenderer {
|
impl ImageRenderer {
|
||||||
@@ -51,65 +58,137 @@ impl ImageRenderer {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create bind group layout for images
|
// Create bind group layout for uniforms (binding 0)
|
||||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
let uniform_layout =
|
||||||
label: Some("Image Bind Group Layout"),
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
label: Some("Image Uniform Layout"),
|
||||||
wgpu::BindGroupLayoutEntry {
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
visibility: wgpu::ShaderStages::VERTEX
|
||||||
|
| wgpu::ShaderStages::FRAGMENT,
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: true,
|
||||||
min_binding_size: None,
|
min_binding_size: None,
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
}],
|
||||||
wgpu::BindGroupLayoutEntry {
|
});
|
||||||
binding: 1,
|
|
||||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
// Create bind group layout for textures (binding 1, 2)
|
||||||
ty: wgpu::BindingType::Texture {
|
let texture_layout =
|
||||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
view_dimension: wgpu::TextureViewDimension::D2,
|
label: Some("Image Texture Layout"),
|
||||||
multisampled: false,
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
sample_type: wgpu::TextureSampleType::Float {
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
multisampled: false,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
},
|
},
|
||||||
count: None,
|
wgpu::BindGroupLayoutEntry {
|
||||||
},
|
binding: 2,
|
||||||
wgpu::BindGroupLayoutEntry {
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
binding: 2,
|
ty: wgpu::BindingType::Sampler(
|
||||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
wgpu::SamplerBindingType::Filtering,
|
||||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
),
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a large uniform buffer for all image renders in a frame
|
||||||
|
// Max 256 images per frame (65536 / 256)
|
||||||
|
let buffer_size = 65536;
|
||||||
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Image Uniform Buffer"),
|
||||||
|
size: buffer_size,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create the uniform bind group
|
||||||
|
let uniform_bind_group =
|
||||||
|
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("Image Uniform Bind Group"),
|
||||||
|
layout: &uniform_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::Buffer(
|
||||||
|
wgpu::BufferBinding {
|
||||||
|
buffer: &uniform_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: std::num::NonZeroU64::new(
|
||||||
|
std::mem::size_of::<ImageUniforms>() as u64,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let alignment =
|
||||||
|
device.limits().min_uniform_buffer_offset_alignment as u64;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
bind_group_layout,
|
uniform_layout,
|
||||||
|
texture_layout,
|
||||||
sampler,
|
sampler,
|
||||||
textures: HashMap::new(),
|
textures: HashMap::new(),
|
||||||
|
uniform_buffer,
|
||||||
|
uniform_bind_group,
|
||||||
|
alignment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the bind group layout for creating the image pipeline.
|
/// Get the uniform bind group layout.
|
||||||
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
|
pub fn uniform_layout(&self) -> &wgpu::BindGroupLayout {
|
||||||
&self.bind_group_layout
|
&self.uniform_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the texture bind group layout.
|
||||||
|
pub fn texture_layout(&self) -> &wgpu::BindGroupLayout {
|
||||||
|
&self.texture_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the uniform bind group.
|
||||||
|
pub fn uniform_bind_group(&self) -> &wgpu::BindGroup {
|
||||||
|
&self.uniform_bind_group
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a GPU image by ID.
|
/// Get a GPU image by ID.
|
||||||
pub fn get(&self, image_id: &u32) -> Option<&GpuImage> {
|
pub fn get(&self, pane_id: PaneId, image_id: &u32) -> Option<&GpuImage> {
|
||||||
self.textures.get(image_id)
|
self.textures.get(&(pane_id, *image_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload an image to the GPU, creating or updating its texture.
|
/// Upload an image to the GPU, creating or updating its texture.
|
||||||
pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, image: &ImageData) {
|
pub fn upload_image(
|
||||||
log::debug!("upload_image: id={}, width={}, height={}, data_len={}", image.id, image.width, image.height, image.data.len());
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
pane_id: PaneId,
|
||||||
|
image: &ImageData,
|
||||||
|
) {
|
||||||
|
log::debug!(
|
||||||
|
"upload_image: pane_id={:?}, id={}, width={}, height={}, data_len={}",
|
||||||
|
pane_id,
|
||||||
|
image.id,
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
image.data.len()
|
||||||
|
);
|
||||||
// Get current frame data (handles animation frames automatically)
|
// Get current frame data (handles animation frames automatically)
|
||||||
let data = image.current_frame_data();
|
let data = image.current_frame_data();
|
||||||
|
|
||||||
// Check if we already have this image
|
// Check if we already have this image
|
||||||
if let Some(existing) = self.textures.get(&image.id) {
|
if let Some(existing) = self.textures.get(&(pane_id, image.id)) {
|
||||||
if existing.width == image.width && existing.height == image.height {
|
if existing.width == image.width && existing.height == image.height
|
||||||
|
{
|
||||||
// Same dimensions, just update the data
|
// Same dimensions, just update the data
|
||||||
queue.write_texture(
|
queue.write_texture(
|
||||||
wgpu::TexelCopyTextureInfo {
|
wgpu::TexelCopyTextureInfo {
|
||||||
@@ -137,7 +216,7 @@ impl ImageRenderer {
|
|||||||
|
|
||||||
// Create new texture
|
// Create new texture
|
||||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some(&format!("Image {}", image.id)),
|
label: Some(&format!("Image {} (pane {:?})", image.id, pane_id)),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
@@ -147,7 +226,8 @@ impl ImageRenderer {
|
|||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
||||||
|
| wgpu::TextureUsages::COPY_DST,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,23 +254,13 @@ impl ImageRenderer {
|
|||||||
|
|
||||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
// Create per-image uniform buffer
|
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
||||||
label: Some(&format!("Image {} Uniform Buffer", image.id)),
|
|
||||||
size: std::mem::size_of::<ImageUniforms>() as u64,
|
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create bind group for this image with its own uniform buffer
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some(&format!("Image {} Bind Group", image.id)),
|
label: Some(&format!(
|
||||||
layout: &self.bind_group_layout,
|
"Image {} (pane {:?}) Bind Group",
|
||||||
|
image.id, pane_id
|
||||||
|
)),
|
||||||
|
layout: &self.texture_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: uniform_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::TextureView(&view),
|
resource: wgpu::BindingResource::TextureView(&view),
|
||||||
@@ -202,14 +272,16 @@ impl ImageRenderer {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
self.textures.insert(image.id, GpuImage {
|
self.textures.insert(
|
||||||
texture,
|
(pane_id, image.id),
|
||||||
view,
|
GpuImage {
|
||||||
uniform_buffer,
|
texture,
|
||||||
bind_group,
|
view,
|
||||||
width: image.width,
|
bind_group,
|
||||||
height: image.height,
|
width: image.width,
|
||||||
});
|
height: image.height,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Uploaded image {} ({}x{}) to GPU",
|
"Uploaded image {} ({}x{}) to GPU",
|
||||||
@@ -220,52 +292,89 @@ impl ImageRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an image from the GPU.
|
/// Remove an image from the GPU.
|
||||||
pub fn remove_image(&mut self, image_id: u32) {
|
pub fn remove_image(&mut self, pane_id: PaneId, image_id: u32) {
|
||||||
if self.textures.remove(&image_id).is_some() {
|
if self.textures.remove(&(pane_id, image_id)).is_some() {
|
||||||
log::debug!("Removed image {} from GPU", image_id);
|
log::debug!(
|
||||||
|
"Removed image {} (pane {:?}) from GPU",
|
||||||
|
image_id,
|
||||||
|
pane_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync images from terminal's image storage to GPU.
|
/// Sync images from terminal's image storage to GPU.
|
||||||
/// Uploads new/changed images and removes deleted ones.
|
/// Uploads new/changed images and removes deleted ones.
|
||||||
/// Also updates animation frames.
|
/// Also updates animation frames.
|
||||||
pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) {
|
pub fn sync_images(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
pane_id: PaneId,
|
||||||
|
storage: &mut ImageStorage,
|
||||||
|
) {
|
||||||
// Update animations and get list of changed image IDs
|
// Update animations and get list of changed image IDs
|
||||||
let changed_ids = storage.update_animations();
|
let changed_ids = storage.update_animations();
|
||||||
log::debug!("Sync images: changed_ids={:?}, dirty={}", changed_ids, storage.dirty);
|
log::debug!(
|
||||||
|
"Sync images: pane_id={:?}, changed_ids={:?}, dirty={}",
|
||||||
|
pane_id,
|
||||||
|
changed_ids,
|
||||||
|
storage.dirty
|
||||||
|
);
|
||||||
|
|
||||||
// Re-upload frames that changed due to animation
|
// Re-upload frames that changed due to animation
|
||||||
for id in &changed_ids {
|
for id in &changed_ids {
|
||||||
if let Some(image) = storage.get_image(*id) {
|
if let Some(image) = storage.get_image(*id) {
|
||||||
self.upload_image(device, queue, image);
|
self.upload_image(device, queue, pane_id, image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !storage.dirty && changed_ids.is_empty() {
|
if !storage.dirty && changed_ids.is_empty() {
|
||||||
|
log::debug!(
|
||||||
|
"Sync images: skipping upload (not dirty, no animations)"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload all images (upload_image handles deduplication)
|
// Upload images that were marked as dirty (newly transmitted or modified)
|
||||||
for image in storage.images().values() {
|
for id in &storage.dirty_images {
|
||||||
self.upload_image(device, queue, image);
|
log::debug!("Sync images: uploading dirty image id={:?}", id);
|
||||||
}
|
if let Some(image) = storage.get_image(*id) {
|
||||||
|
self.upload_image(device, queue, pane_id, image);
|
||||||
// Remove textures for deleted images
|
|
||||||
let current_ids: std::collections::HashSet<u32> = storage.images().keys().copied().collect();
|
|
||||||
let gpu_ids: Vec<u32> = self.textures.keys().copied().collect();
|
|
||||||
for id in gpu_ids {
|
|
||||||
if !current_ids.contains(&id) {
|
|
||||||
self.remove_image(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.clear_dirty();
|
storage.clear_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove images from the GPU that are not present in the provided set of active images.
|
||||||
|
/// active_images is a set of (pane_id, image_id) tuples.
|
||||||
|
pub fn gc_images(
|
||||||
|
&mut self,
|
||||||
|
active_images: &std::collections::HashSet<(PaneId, u32)>,
|
||||||
|
) {
|
||||||
|
let gpu_ids: Vec<(PaneId, u32)> =
|
||||||
|
self.textures.keys().copied().collect();
|
||||||
|
let mut removed_count = 0;
|
||||||
|
for id in gpu_ids {
|
||||||
|
if !active_images.contains(&id) {
|
||||||
|
log::debug!(
|
||||||
|
"GC: removing image {:?} as it is no longer active",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
self.remove_image(id.0, id.1);
|
||||||
|
removed_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if removed_count > 0 {
|
||||||
|
log::debug!("GC images: removed {} unused textures", removed_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Prepare image renders for a pane.
|
/// Prepare image renders for a pane.
|
||||||
/// Returns a Vec of (image_id, uniforms) for deferred rendering.
|
/// Returns a Vec of (image_id, uniforms) for deferred rendering.
|
||||||
pub fn prepare_image_renders(
|
pub fn prepare_image_renders(
|
||||||
&self,
|
&self,
|
||||||
|
pane_id: PaneId,
|
||||||
placements: &[ImagePlacement],
|
placements: &[ImagePlacement],
|
||||||
pane_x: f32,
|
pane_x: f32,
|
||||||
pane_y: f32,
|
pane_y: f32,
|
||||||
@@ -276,37 +385,69 @@ impl ImageRenderer {
|
|||||||
scrollback_len: usize,
|
scrollback_len: usize,
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
visible_rows: usize,
|
visible_rows: usize,
|
||||||
|
dim_factor: f32,
|
||||||
) -> Vec<(u32, ImageUniforms)> {
|
) -> Vec<(u32, ImageUniforms)> {
|
||||||
|
log::debug!(
|
||||||
|
"prepare_image_renders: pane={:?}, placements={}, scrollback={}, offset={}, rows={}",
|
||||||
|
pane_id,
|
||||||
|
placements.len(),
|
||||||
|
scrollback_len,
|
||||||
|
scroll_offset,
|
||||||
|
visible_rows
|
||||||
|
);
|
||||||
let mut renders = Vec::new();
|
let mut renders = Vec::new();
|
||||||
|
|
||||||
for placement in placements {
|
for placement in placements {
|
||||||
// Check if we have the GPU texture for this image
|
// Check if we have the GPU texture for this image
|
||||||
let gpu_image = match self.textures.get(&placement.image_id) {
|
let gpu_image =
|
||||||
Some(img) => img,
|
match self.textures.get(&(pane_id, placement.image_id)) {
|
||||||
None => continue, // Skip if not uploaded yet
|
Some(img) => img,
|
||||||
};
|
None => {
|
||||||
|
log::debug!(
|
||||||
|
"Image {} not found in GPU cache for pane {:?}",
|
||||||
|
placement.image_id,
|
||||||
|
pane_id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Convert absolute row to visible screen row
|
// Convert absolute row to visible screen row
|
||||||
// placement.row is absolute (scrollback_len_at_placement + cursor_row)
|
// placement.row is absolute (scrollback_len_at_placement + cursor_row)
|
||||||
// visible_row = absolute_row - scrollback_len + scroll_offset
|
// visible_row = absolute_row - scrollback_len + scroll_offset
|
||||||
let absolute_row = placement.row as isize;
|
let absolute_row = placement.row as isize;
|
||||||
let visible_row = absolute_row - scrollback_len as isize + scroll_offset as isize;
|
let visible_row =
|
||||||
|
absolute_row - scrollback_len as isize + scroll_offset as isize;
|
||||||
|
|
||||||
// Check if image is visible on screen
|
// Check if image is visible on screen
|
||||||
// Image spans from visible_row to visible_row + placement.rows
|
// Image spans from visible_row to visible_row + placement.rows
|
||||||
let image_bottom = visible_row + placement.rows as isize;
|
let image_bottom = visible_row + placement.rows as isize;
|
||||||
if image_bottom < 0 || visible_row >= visible_rows as isize {
|
if image_bottom < 0 || visible_row >= visible_rows as isize {
|
||||||
log::debug!("Image {} culled: visible_row={}, image_bottom={}, visible_rows={}", placement.image_id, visible_row, image_bottom, visible_rows);
|
log::debug!(
|
||||||
|
"Image {} culled: visible_row={}, image_bottom={}, visible_rows={}",
|
||||||
|
placement.image_id,
|
||||||
|
visible_row,
|
||||||
|
image_bottom,
|
||||||
|
visible_rows
|
||||||
|
);
|
||||||
continue; // Image is completely off-screen
|
continue; // Image is completely off-screen
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate display position in pixels
|
// Calculate display position in pixels
|
||||||
let pos_x = pane_x + (placement.col as f32 * cell_width) + placement.x_offset as f32;
|
let pos_x = pane_x
|
||||||
let pos_y = pane_y + (visible_row as f32 * cell_height) + placement.y_offset as f32;
|
+ (placement.col as f32 * cell_width)
|
||||||
|
+ placement.x_offset as f32;
|
||||||
|
let pos_y = pane_y
|
||||||
|
+ (visible_row as f32 * cell_height)
|
||||||
|
+ placement.y_offset as f32;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Image render: pane_x={} col={} cell_width={} x_offset={} => pos_x={}",
|
"Image render: pane_x={} col={} cell_width={} x_offset={} => pos_x={}",
|
||||||
pane_x, placement.col, cell_width, placement.x_offset, pos_x
|
pane_x,
|
||||||
|
placement.col,
|
||||||
|
cell_width,
|
||||||
|
placement.x_offset,
|
||||||
|
pos_x
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate display size in pixels
|
// Calculate display size in pixels
|
||||||
@@ -338,8 +479,8 @@ impl ImageRenderer {
|
|||||||
src_y,
|
src_y,
|
||||||
src_width,
|
src_width,
|
||||||
src_height,
|
src_height,
|
||||||
|
dim_factor,
|
||||||
_padding1: 0.0,
|
_padding1: 0.0,
|
||||||
_padding2: 0.0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renders.push((placement.image_id, uniforms));
|
renders.push((placement.image_id, uniforms));
|
||||||
|
|||||||
@@ -16,18 +16,19 @@ struct ImageUniforms {
|
|||||||
src_y: f32,
|
src_y: f32,
|
||||||
src_width: f32,
|
src_width: f32,
|
||||||
src_height: f32,
|
src_height: f32,
|
||||||
|
// Dim factor for unfocused panes (1.0 = bright, 0.0 = dimmed)
|
||||||
|
dim_factor: f32,
|
||||||
// Padding for alignment
|
// Padding for alignment
|
||||||
_padding1: f32,
|
_padding1: f32,
|
||||||
_padding2: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> uniforms: ImageUniforms;
|
var<uniform> uniforms: ImageUniforms;
|
||||||
|
|
||||||
@group(0) @binding(1)
|
@group(1) @binding(1)
|
||||||
var image_texture: texture_2d<f32>;
|
var image_texture: texture_2d<f32>;
|
||||||
|
|
||||||
@group(0) @binding(2)
|
@group(1) @binding(2)
|
||||||
var image_sampler: sampler;
|
var image_sampler: sampler;
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@@ -87,6 +88,9 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
// Sample the image texture
|
// Sample the image texture
|
||||||
let color = textureSample(image_texture, image_sampler, in.uv);
|
let color = textureSample(image_texture, image_sampler, in.uv);
|
||||||
|
|
||||||
|
// Apply dimming factor to RGB channels
|
||||||
|
let dimmed_rgb = color.rgb * uniforms.dim_factor;
|
||||||
|
|
||||||
// Return with premultiplied alpha for proper blending
|
// Return with premultiplied alpha for proper blending
|
||||||
return vec4<f32>(color.rgb * color.a, color.a);
|
return vec4<f32>(dimmed_rgb * color.a, color.a);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -73,11 +73,7 @@ impl Modifiers {
|
|||||||
bits |= 128;
|
bits |= 128;
|
||||||
}
|
}
|
||||||
|
|
||||||
if bits == 0 {
|
if bits == 0 { None } else { Some(1 + bits) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(1 + bits)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if any modifier is active.
|
/// Returns true if any modifier is active.
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ pub mod box_drawing;
|
|||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod color_font;
|
pub mod color_font;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod font_loader;
|
|
||||||
pub mod edge_glow;
|
pub mod edge_glow;
|
||||||
|
pub mod font_loader;
|
||||||
pub mod gpu_types;
|
pub mod gpu_types;
|
||||||
pub mod graphics;
|
pub mod graphics;
|
||||||
pub mod image_renderer;
|
pub mod image_renderer;
|
||||||
@@ -16,8 +16,8 @@ pub mod pane_resources;
|
|||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
pub mod simd_utf8;
|
||||||
pub mod statusline;
|
pub mod statusline;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod simd_utf8;
|
|
||||||
pub mod vt_parser;
|
pub mod vt_parser;
|
||||||
mod vt_test_osc;
|
mod vt_test_osc;
|
||||||
|
|||||||
+484
-240
File diff suppressed because it is too large
Load Diff
+57
-36
@@ -24,12 +24,30 @@ impl<'a> PipelineBuilder<'a> {
|
|||||||
layout: &'a wgpu::PipelineLayout,
|
layout: &'a wgpu::PipelineLayout,
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { device, shader, layout, format }
|
Self {
|
||||||
|
device,
|
||||||
|
shader,
|
||||||
|
layout,
|
||||||
|
format,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a pipeline with TriangleStrip topology and no vertex buffers (most common case).
|
/// Build a pipeline with TriangleStrip topology and no vertex buffers (most common case).
|
||||||
pub fn build(&self, label: &str, vs_entry: &str, fs_entry: &str, blend: wgpu::BlendState) -> wgpu::RenderPipeline {
|
pub fn build(
|
||||||
self.build_full(label, vs_entry, fs_entry, blend, wgpu::PrimitiveTopology::TriangleStrip, &[])
|
&self,
|
||||||
|
label: &str,
|
||||||
|
vs_entry: &str,
|
||||||
|
fs_entry: &str,
|
||||||
|
blend: wgpu::BlendState,
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
self.build_full(
|
||||||
|
label,
|
||||||
|
vs_entry,
|
||||||
|
fs_entry,
|
||||||
|
blend,
|
||||||
|
wgpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
&[],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a pipeline with custom topology and vertex buffers.
|
/// Build a pipeline with custom topology and vertex buffers.
|
||||||
@@ -42,38 +60,41 @@ impl<'a> PipelineBuilder<'a> {
|
|||||||
topology: wgpu::PrimitiveTopology,
|
topology: wgpu::PrimitiveTopology,
|
||||||
vertex_buffers: &[wgpu::VertexBufferLayout<'_>],
|
vertex_buffers: &[wgpu::VertexBufferLayout<'_>],
|
||||||
) -> wgpu::RenderPipeline {
|
) -> wgpu::RenderPipeline {
|
||||||
self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
self.device
|
||||||
label: Some(label),
|
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
layout: Some(self.layout),
|
label: Some(label),
|
||||||
vertex: wgpu::VertexState {
|
layout: Some(self.layout),
|
||||||
module: self.shader,
|
vertex: wgpu::VertexState {
|
||||||
entry_point: Some(vs_entry),
|
module: self.shader,
|
||||||
buffers: vertex_buffers,
|
entry_point: Some(vs_entry),
|
||||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
buffers: vertex_buffers,
|
||||||
},
|
compilation_options:
|
||||||
fragment: Some(wgpu::FragmentState {
|
wgpu::PipelineCompilationOptions::default(),
|
||||||
module: self.shader,
|
},
|
||||||
entry_point: Some(fs_entry),
|
fragment: Some(wgpu::FragmentState {
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
module: self.shader,
|
||||||
format: self.format,
|
entry_point: Some(fs_entry),
|
||||||
blend: Some(blend),
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
format: self.format,
|
||||||
})],
|
blend: Some(blend),
|
||||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
}),
|
})],
|
||||||
primitive: wgpu::PrimitiveState {
|
compilation_options:
|
||||||
topology,
|
wgpu::PipelineCompilationOptions::default(),
|
||||||
strip_index_format: None,
|
}),
|
||||||
front_face: wgpu::FrontFace::Ccw,
|
primitive: wgpu::PrimitiveState {
|
||||||
cull_mode: None,
|
topology,
|
||||||
polygon_mode: wgpu::PolygonMode::Fill,
|
strip_index_format: None,
|
||||||
unclipped_depth: false,
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
conservative: false,
|
cull_mode: None,
|
||||||
},
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
depth_stencil: None,
|
unclipped_depth: false,
|
||||||
multisample: wgpu::MultisampleState::default(),
|
conservative: false,
|
||||||
multiview_mask: None,
|
},
|
||||||
cache: None,
|
depth_stencil: None,
|
||||||
})
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-17
@@ -1,8 +1,8 @@
|
|||||||
//! PTY (pseudo-terminal) handling for shell communication.
|
//! PTY (pseudo-terminal) handling for shell communication.
|
||||||
|
|
||||||
use rustix::fs::{fcntl_setfl, OFlags};
|
use rustix::fs::{OFlags, fcntl_setfl};
|
||||||
use rustix::io::{read, write, Errno};
|
use rustix::io::{Errno, read, write};
|
||||||
use rustix::pty::{grantpt, openpt, ptsname, unlockpt, OpenptFlags};
|
use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt};
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -36,20 +36,30 @@ pub struct Pty {
|
|||||||
impl Pty {
|
impl Pty {
|
||||||
/// Creates a new PTY and spawns a shell process.
|
/// Creates a new PTY and spawns a shell process.
|
||||||
/// The initial terminal size should be provided so the shell starts with the correct dimensions.
|
/// The initial terminal size should be provided so the shell starts with the correct dimensions.
|
||||||
pub fn spawn(shell: Option<&str>, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<Self, PtyError> {
|
pub fn spawn(
|
||||||
|
shell: Option<&str>,
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
xpixel: u16,
|
||||||
|
ypixel: u16,
|
||||||
|
) -> Result<Self, PtyError> {
|
||||||
// Open the PTY master
|
// Open the PTY master
|
||||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
|
let master = openpt(
|
||||||
.map_err(PtyError::OpenMaster)?;
|
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
|
||||||
|
)
|
||||||
|
.map_err(PtyError::OpenMaster)?;
|
||||||
|
|
||||||
// Set non-blocking mode on master
|
// Set non-blocking mode on master
|
||||||
fcntl_setfl(&master, OFlags::NONBLOCK).map_err(|e| PtyError::Io(e.into()))?;
|
fcntl_setfl(&master, OFlags::NONBLOCK)
|
||||||
|
.map_err(|e| PtyError::Io(e.into()))?;
|
||||||
|
|
||||||
// Grant and unlock the PTY
|
// Grant and unlock the PTY
|
||||||
grantpt(&master).map_err(PtyError::Grant)?;
|
grantpt(&master).map_err(PtyError::Grant)?;
|
||||||
unlockpt(&master).map_err(PtyError::Unlock)?;
|
unlockpt(&master).map_err(PtyError::Unlock)?;
|
||||||
|
|
||||||
// Get the slave name
|
// Get the slave name
|
||||||
let slave_name = ptsname(&master, Vec::new()).map_err(PtyError::PtsName)?;
|
let slave_name =
|
||||||
|
ptsname(&master, Vec::new()).map_err(PtyError::PtsName)?;
|
||||||
|
|
||||||
// Set the terminal size BEFORE forking so the child inherits the correct size.
|
// Set the terminal size BEFORE forking so the child inherits the correct size.
|
||||||
// This prevents race conditions where the shell's .zshrc runs before the parent
|
// This prevents race conditions where the shell's .zshrc runs before the parent
|
||||||
@@ -78,7 +88,8 @@ impl Pty {
|
|||||||
}
|
}
|
||||||
pid => {
|
pid => {
|
||||||
// Parent process
|
// Parent process
|
||||||
let child_pid = unsafe { rustix::process::Pid::from_raw_unchecked(pid) };
|
let child_pid =
|
||||||
|
unsafe { rustix::process::Pid::from_raw_unchecked(pid) };
|
||||||
Ok(Self { master, child_pid })
|
Ok(Self { master, child_pid })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,14 +136,16 @@ impl Pty {
|
|||||||
.or_else(|| std::env::var("SHELL").ok())
|
.or_else(|| std::env::var("SHELL").ok())
|
||||||
.unwrap_or_else(|| "/bin/sh".to_string());
|
.unwrap_or_else(|| "/bin/sh".to_string());
|
||||||
|
|
||||||
let shell_cstr = CString::new(shell_path.clone()).expect("Invalid shell path");
|
let shell_cstr =
|
||||||
|
CString::new(shell_path.clone()).expect("Invalid shell path");
|
||||||
let shell_name = std::path::Path::new(&shell_path)
|
let shell_name = std::path::Path::new(&shell_path)
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("sh");
|
.unwrap_or("sh");
|
||||||
|
|
||||||
// Login shell (prepend with -)
|
// Login shell (prepend with -)
|
||||||
let login_shell = CString::new(format!("-{}", shell_name)).expect("Invalid shell name");
|
let login_shell = CString::new(format!("-{}", shell_name))
|
||||||
|
.expect("Invalid shell name");
|
||||||
|
|
||||||
// Execute the shell
|
// Execute the shell
|
||||||
let args = [login_shell.as_ptr(), std::ptr::null()];
|
let args = [login_shell.as_ptr(), std::ptr::null()];
|
||||||
@@ -166,7 +179,13 @@ impl Pty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resizes the PTY window.
|
/// Resizes the PTY window.
|
||||||
pub fn resize(&self, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<(), PtyError> {
|
pub fn resize(
|
||||||
|
&self,
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
xpixel: u16,
|
||||||
|
ypixel: u16,
|
||||||
|
) -> Result<(), PtyError> {
|
||||||
let winsize = libc::winsize {
|
let winsize = libc::winsize {
|
||||||
ws_row: rows,
|
ws_row: rows,
|
||||||
ws_col: cols,
|
ws_col: cols,
|
||||||
@@ -210,11 +229,7 @@ impl Pty {
|
|||||||
pub fn foreground_pgid(&self) -> Option<i32> {
|
pub fn foreground_pgid(&self) -> Option<i32> {
|
||||||
let fd = self.master.as_raw_fd();
|
let fd = self.master.as_raw_fd();
|
||||||
let pgid = unsafe { libc::tcgetpgrp(fd) };
|
let pgid = unsafe { libc::tcgetpgrp(fd) };
|
||||||
if pgid > 0 {
|
if pgid > 0 { Some(pgid) } else { None }
|
||||||
Some(pgid)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the name of the foreground process running in this PTY.
|
/// Get the name of the foreground process running in this PTY.
|
||||||
|
|||||||
+2176
-1027
File diff suppressed because it is too large
Load Diff
+315
-95
@@ -53,7 +53,8 @@ impl SimdCapabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global cached capabilities (initialized on first use)
|
// Global cached capabilities (initialized on first use)
|
||||||
static SIMD_CAPS: std::sync::OnceLock<SimdCapabilities> = std::sync::OnceLock::new();
|
static SIMD_CAPS: std::sync::OnceLock<SimdCapabilities> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
/// Get cached SIMD capabilities.
|
/// Get cached SIMD capabilities.
|
||||||
pub fn simd_caps() -> &'static SimdCapabilities {
|
pub fn simd_caps() -> &'static SimdCapabilities {
|
||||||
@@ -156,7 +157,11 @@ unsafe fn find_byte_avx2(haystack: &[u8], needle: u8) -> Option<usize> {
|
|||||||
///
|
///
|
||||||
/// This is equivalent to Kitty's `find_either_of_two_bytes` function.
|
/// This is equivalent to Kitty's `find_either_of_two_bytes` function.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn find_either_of_two_bytes(haystack: &[u8], a: u8, b: u8) -> Option<usize> {
|
pub fn find_either_of_two_bytes(
|
||||||
|
haystack: &[u8],
|
||||||
|
a: u8,
|
||||||
|
b: u8,
|
||||||
|
) -> Option<usize> {
|
||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
{
|
{
|
||||||
let caps = simd_caps();
|
let caps = simd_caps();
|
||||||
@@ -174,14 +179,22 @@ pub fn find_either_of_two_bytes(haystack: &[u8], a: u8, b: u8) -> Option<usize>
|
|||||||
|
|
||||||
/// Scalar fallback for find_either_of_two_bytes.
|
/// Scalar fallback for find_either_of_two_bytes.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn find_either_of_two_bytes_scalar(haystack: &[u8], a: u8, b: u8) -> Option<usize> {
|
fn find_either_of_two_bytes_scalar(
|
||||||
|
haystack: &[u8],
|
||||||
|
a: u8,
|
||||||
|
b: u8,
|
||||||
|
) -> Option<usize> {
|
||||||
haystack.iter().position(|&byte| byte == a || byte == b)
|
haystack.iter().position(|&byte| byte == a || byte == b)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSE implementation of find_either_of_two_bytes.
|
/// SSE implementation of find_either_of_two_bytes.
|
||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
#[target_feature(enable = "sse2", enable = "sse4.1")]
|
#[target_feature(enable = "sse2", enable = "sse4.1")]
|
||||||
unsafe fn find_either_of_two_bytes_sse(haystack: &[u8], a: u8, b: u8) -> Option<usize> {
|
unsafe fn find_either_of_two_bytes_sse(
|
||||||
|
haystack: &[u8],
|
||||||
|
a: u8,
|
||||||
|
b: u8,
|
||||||
|
) -> Option<usize> {
|
||||||
let a_vec = _mm_set1_epi8(a as i8);
|
let a_vec = _mm_set1_epi8(a as i8);
|
||||||
let b_vec = _mm_set1_epi8(b as i8);
|
let b_vec = _mm_set1_epi8(b as i8);
|
||||||
|
|
||||||
@@ -215,7 +228,11 @@ unsafe fn find_either_of_two_bytes_sse(haystack: &[u8], a: u8, b: u8) -> Option<
|
|||||||
/// AVX2 implementation of find_either_of_two_bytes.
|
/// AVX2 implementation of find_either_of_two_bytes.
|
||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
#[target_feature(enable = "avx2")]
|
#[target_feature(enable = "avx2")]
|
||||||
unsafe fn find_either_of_two_bytes_avx2(haystack: &[u8], a: u8, b: u8) -> Option<usize> {
|
unsafe fn find_either_of_two_bytes_avx2(
|
||||||
|
haystack: &[u8],
|
||||||
|
a: u8,
|
||||||
|
b: u8,
|
||||||
|
) -> Option<usize> {
|
||||||
let a_vec = _mm256_set1_epi8(a as i8);
|
let a_vec = _mm256_set1_epi8(a as i8);
|
||||||
let b_vec = _mm256_set1_epi8(b as i8);
|
let b_vec = _mm256_set1_epi8(b as i8);
|
||||||
|
|
||||||
@@ -289,7 +306,9 @@ pub fn find_c0_control(haystack: &[u8]) -> Option<usize> {
|
|||||||
/// Scalar fallback for find_c0_control.
|
/// Scalar fallback for find_c0_control.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn find_c0_control_scalar(haystack: &[u8]) -> Option<usize> {
|
fn find_c0_control_scalar(haystack: &[u8]) -> Option<usize> {
|
||||||
haystack.iter().position(|&byte| byte < 0x20 || byte == 0x7F)
|
haystack
|
||||||
|
.iter()
|
||||||
|
.position(|&byte| byte < 0x20 || byte == 0x7F)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSE implementation of find_c0_control.
|
/// SSE implementation of find_c0_control.
|
||||||
@@ -417,7 +436,11 @@ pub fn xor_mask(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize {
|
|||||||
|
|
||||||
/// Scalar fallback for xor_mask.
|
/// Scalar fallback for xor_mask.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn xor_mask_scalar(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize {
|
fn xor_mask_scalar(
|
||||||
|
data: &mut [u8],
|
||||||
|
mask: [u8; 4],
|
||||||
|
start_offset: usize,
|
||||||
|
) -> usize {
|
||||||
let mut offset = start_offset;
|
let mut offset = start_offset;
|
||||||
for byte in data.iter_mut() {
|
for byte in data.iter_mut() {
|
||||||
*byte ^= mask[offset & 3];
|
*byte ^= mask[offset & 3];
|
||||||
@@ -429,7 +452,11 @@ fn xor_mask_scalar(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize
|
|||||||
/// SSE implementation of xor_mask.
|
/// SSE implementation of xor_mask.
|
||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
#[target_feature(enable = "sse2")]
|
#[target_feature(enable = "sse2")]
|
||||||
unsafe fn xor_mask_sse(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize {
|
unsafe fn xor_mask_sse(
|
||||||
|
data: &mut [u8],
|
||||||
|
mask: [u8; 4],
|
||||||
|
start_offset: usize,
|
||||||
|
) -> usize {
|
||||||
let len = data.len();
|
let len = data.len();
|
||||||
let ptr = data.as_mut_ptr();
|
let ptr = data.as_mut_ptr();
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
@@ -444,10 +471,22 @@ unsafe fn xor_mask_sse(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> u
|
|||||||
|
|
||||||
// Create 16-byte mask vector (repeat 4-byte mask 4 times)
|
// Create 16-byte mask vector (repeat 4-byte mask 4 times)
|
||||||
let mask_vec = _mm_set_epi8(
|
let mask_vec = _mm_set_epi8(
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[3] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[2] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[1] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process 16 bytes at a time
|
// Process 16 bytes at a time
|
||||||
@@ -472,7 +511,11 @@ unsafe fn xor_mask_sse(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> u
|
|||||||
/// AVX2 implementation of xor_mask.
|
/// AVX2 implementation of xor_mask.
|
||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
#[target_feature(enable = "avx2")]
|
#[target_feature(enable = "avx2")]
|
||||||
unsafe fn xor_mask_avx2(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize {
|
unsafe fn xor_mask_avx2(
|
||||||
|
data: &mut [u8],
|
||||||
|
mask: [u8; 4],
|
||||||
|
start_offset: usize,
|
||||||
|
) -> usize {
|
||||||
let len = data.len();
|
let len = data.len();
|
||||||
let ptr = data.as_mut_ptr();
|
let ptr = data.as_mut_ptr();
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
@@ -487,14 +530,38 @@ unsafe fn xor_mask_avx2(data: &mut [u8], mask: [u8; 4], start_offset: usize) ->
|
|||||||
|
|
||||||
// Create 32-byte mask vector (repeat 4-byte mask 8 times)
|
// Create 32-byte mask vector (repeat 4-byte mask 8 times)
|
||||||
let mask_vec = _mm256_set_epi8(
|
let mask_vec = _mm256_set_epi8(
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[3] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[2] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[1] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[0] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[3] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[2] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[1] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process 32 bytes at a time
|
// Process 32 bytes at a time
|
||||||
@@ -509,10 +576,22 @@ unsafe fn xor_mask_avx2(data: &mut [u8], mask: [u8; 4], start_offset: usize) ->
|
|||||||
// Process 16 bytes if remaining
|
// Process 16 bytes if remaining
|
||||||
while pos + 16 <= len {
|
while pos + 16 <= len {
|
||||||
let mask_vec_128 = _mm_set_epi8(
|
let mask_vec_128 = _mm_set_epi8(
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[3] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[2] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[1] as i8,
|
||||||
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8,
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
|
mask[3] as i8,
|
||||||
|
mask[2] as i8,
|
||||||
|
mask[1] as i8,
|
||||||
|
mask[0] as i8,
|
||||||
);
|
);
|
||||||
let chunk = _mm_loadu_si128(ptr.add(pos) as *const __m128i);
|
let chunk = _mm_loadu_si128(ptr.add(pos) as *const __m128i);
|
||||||
let xored = _mm_xor_si128(chunk, mask_vec_128);
|
let xored = _mm_xor_si128(chunk, mask_vec_128);
|
||||||
@@ -549,20 +628,23 @@ const UTF8_REJECT: u8 = 12;
|
|||||||
/// UTF-8 state transition table (Bjoern Hoehrmann's DFA).
|
/// UTF-8 state transition table (Bjoern Hoehrmann's DFA).
|
||||||
static UTF8_DECODE_TABLE: [u8; 364] = [
|
static UTF8_DECODE_TABLE: [u8; 364] = [
|
||||||
// Character class lookup (0-255)
|
// Character class lookup (0-255)
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
|
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9,
|
||||||
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 2, 2, 2, 2, 2, 2,
|
||||||
// State transition table
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10,
|
||||||
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8,
|
||||||
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
|
8, 8, 8, 8, 8, 8, // State transition table
|
||||||
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
|
0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12,
|
||||||
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
|
12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24,
|
||||||
12,36,12,12,12,12,12,12,12,12,12,12,
|
12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12,
|
||||||
|
12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12,
|
||||||
|
12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12,
|
||||||
|
12, 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Decode a single UTF-8 byte using DFA.
|
/// Decode a single UTF-8 byte using DFA.
|
||||||
@@ -604,7 +686,11 @@ impl SimdUtf8Decoder {
|
|||||||
/// Output codepoints are written to the output buffer as u32 values.
|
/// Output codepoints are written to the output buffer as u32 values.
|
||||||
/// Uses AVX2 (32 bytes at a time) if available, otherwise SSE (16 bytes).
|
/// Uses AVX2 (32 bytes at a time) if available, otherwise SSE (16 bytes).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn decode_to_esc(&mut self, src: &[u8], output: &mut Vec<u32>) -> (usize, bool) {
|
pub fn decode_to_esc(
|
||||||
|
&mut self,
|
||||||
|
src: &[u8],
|
||||||
|
output: &mut Vec<u32>,
|
||||||
|
) -> (usize, bool) {
|
||||||
output.clear();
|
output.clear();
|
||||||
if src.is_empty() {
|
if src.is_empty() {
|
||||||
return (0, false);
|
return (0, false);
|
||||||
@@ -627,7 +713,11 @@ impl SimdUtf8Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scalar fallback decoder.
|
/// Scalar fallback decoder.
|
||||||
fn decode_to_esc_scalar(&mut self, src: &[u8], output: &mut Vec<u32>) -> (usize, bool) {
|
fn decode_to_esc_scalar(
|
||||||
|
&mut self,
|
||||||
|
src: &[u8],
|
||||||
|
output: &mut Vec<u32>,
|
||||||
|
) -> (usize, bool) {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
|
|
||||||
while pos < src.len() {
|
while pos < src.len() {
|
||||||
@@ -644,7 +734,11 @@ impl SimdUtf8Decoder {
|
|||||||
pos += 1;
|
pos += 1;
|
||||||
self.state.prev = self.state.cur;
|
self.state.prev = self.state.cur;
|
||||||
|
|
||||||
match decode_utf8_byte(&mut self.state.cur, &mut self.state.codep, byte) {
|
match decode_utf8_byte(
|
||||||
|
&mut self.state.cur,
|
||||||
|
&mut self.state.codep,
|
||||||
|
byte,
|
||||||
|
) {
|
||||||
UTF8_ACCEPT => {
|
UTF8_ACCEPT => {
|
||||||
output.push(self.state.codep);
|
output.push(self.state.codep);
|
||||||
}
|
}
|
||||||
@@ -667,7 +761,11 @@ impl SimdUtf8Decoder {
|
|||||||
/// Based on Kitty's simd-string-impl.h
|
/// Based on Kitty's simd-string-impl.h
|
||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
#[target_feature(enable = "sse2", enable = "ssse3", enable = "sse4.1")]
|
#[target_feature(enable = "sse2", enable = "ssse3", enable = "sse4.1")]
|
||||||
unsafe fn decode_to_esc_simd(&mut self, src: &[u8], output: &mut Vec<u32>) -> (usize, bool) {
|
unsafe fn decode_to_esc_simd(
|
||||||
|
&mut self,
|
||||||
|
src: &[u8],
|
||||||
|
output: &mut Vec<u32>,
|
||||||
|
) -> (usize, bool) {
|
||||||
let mut num_consumed: usize = 0;
|
let mut num_consumed: usize = 0;
|
||||||
|
|
||||||
// Finish any trailing sequence from previous call
|
// Finish any trailing sequence from previous call
|
||||||
@@ -685,7 +783,8 @@ impl SimdUtf8Decoder {
|
|||||||
let two = _mm_set1_epi8(2);
|
let two = _mm_set1_epi8(2);
|
||||||
let three = _mm_set1_epi8(3);
|
let three = _mm_set1_epi8(3);
|
||||||
let four = _mm_set1_epi8(4);
|
let four = _mm_set1_epi8(4);
|
||||||
let numbered = _mm_set_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
|
let numbered =
|
||||||
|
_mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
|
||||||
|
|
||||||
let limit = src.as_ptr().add(src.len());
|
let limit = src.as_ptr().add(src.len());
|
||||||
let mut p = src.as_ptr().add(num_consumed);
|
let mut p = src.as_ptr().add(num_consumed);
|
||||||
@@ -701,7 +800,11 @@ impl SimdUtf8Decoder {
|
|||||||
} else {
|
} else {
|
||||||
// Partial load - zero-extend
|
// Partial load - zero-extend
|
||||||
let mut buf = [0u8; 16];
|
let mut buf = [0u8; 16];
|
||||||
std::ptr::copy_nonoverlapping(p, buf.as_mut_ptr(), chunk_src_sz);
|
std::ptr::copy_nonoverlapping(
|
||||||
|
p,
|
||||||
|
buf.as_mut_ptr(),
|
||||||
|
chunk_src_sz,
|
||||||
|
);
|
||||||
_mm_loadu_si128(buf.as_ptr() as *const __m128i)
|
_mm_loadu_si128(buf.as_ptr() as *const __m128i)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -712,7 +815,9 @@ impl SimdUtf8Decoder {
|
|||||||
let esc_cmp = _mm_cmpeq_epi8(vec, esc_vec);
|
let esc_cmp = _mm_cmpeq_epi8(vec, esc_vec);
|
||||||
let num_bytes_to_first_esc = Self::bytes_to_first_match(esc_cmp);
|
let num_bytes_to_first_esc = Self::bytes_to_first_match(esc_cmp);
|
||||||
|
|
||||||
if num_bytes_to_first_esc >= 0 && (num_bytes_to_first_esc as usize) < chunk_src_sz {
|
if num_bytes_to_first_esc >= 0
|
||||||
|
&& (num_bytes_to_first_esc as usize) < chunk_src_sz
|
||||||
|
{
|
||||||
sentinel_found = true;
|
sentinel_found = true;
|
||||||
chunk_src_sz = num_bytes_to_first_esc as usize;
|
chunk_src_sz = num_bytes_to_first_esc as usize;
|
||||||
num_consumed += chunk_src_sz + 1; // +1 for ESC
|
num_consumed += chunk_src_sz + 1; // +1 for ESC
|
||||||
@@ -754,16 +859,37 @@ impl SimdUtf8Decoder {
|
|||||||
let mut state = state_80;
|
let mut state = state_80;
|
||||||
|
|
||||||
// 2-byte sequence starters (0xC0-0xDF, but 0xC0-0xC1 invalid)
|
// 2-byte sequence starters (0xC0-0xDF, but 0xC0-0xC1 invalid)
|
||||||
let c2_start = _mm_cmplt_epi8(_mm_set1_epi8((0xC0 - 1 - 0x80) as i8), vec_signed);
|
let c2_start = _mm_cmplt_epi8(
|
||||||
state = _mm_blendv_epi8(state, _mm_set1_epi8(0xC2u8 as i8), c2_start);
|
_mm_set1_epi8((0xC0 - 1 - 0x80) as i8),
|
||||||
|
vec_signed,
|
||||||
|
);
|
||||||
|
state = _mm_blendv_epi8(
|
||||||
|
state,
|
||||||
|
_mm_set1_epi8(0xC2u8 as i8),
|
||||||
|
c2_start,
|
||||||
|
);
|
||||||
|
|
||||||
// 3-byte sequence starters (0xE0-0xEF)
|
// 3-byte sequence starters (0xE0-0xEF)
|
||||||
let e3_start = _mm_cmplt_epi8(_mm_set1_epi8((0xE0 - 1 - 0x80) as i8), vec_signed);
|
let e3_start = _mm_cmplt_epi8(
|
||||||
state = _mm_blendv_epi8(state, _mm_set1_epi8(0xE3u8 as i8), e3_start);
|
_mm_set1_epi8((0xE0 - 1 - 0x80) as i8),
|
||||||
|
vec_signed,
|
||||||
|
);
|
||||||
|
state = _mm_blendv_epi8(
|
||||||
|
state,
|
||||||
|
_mm_set1_epi8(0xE3u8 as i8),
|
||||||
|
e3_start,
|
||||||
|
);
|
||||||
|
|
||||||
// 4-byte sequence starters (0xF0-0xFF, but 0xF5+ invalid)
|
// 4-byte sequence starters (0xF0-0xFF, but 0xF5+ invalid)
|
||||||
let f4_start = _mm_cmplt_epi8(_mm_set1_epi8((0xF0 - 1 - 0x80) as i8), vec_signed);
|
let f4_start = _mm_cmplt_epi8(
|
||||||
state = _mm_blendv_epi8(state, _mm_set1_epi8(0xF4u8 as i8), f4_start);
|
_mm_set1_epi8((0xF0 - 1 - 0x80) as i8),
|
||||||
|
vec_signed,
|
||||||
|
);
|
||||||
|
state = _mm_blendv_epi8(
|
||||||
|
state,
|
||||||
|
_mm_set1_epi8(0xF4u8 as i8),
|
||||||
|
f4_start,
|
||||||
|
);
|
||||||
|
|
||||||
// mask = upper 5 bits of state (indicates byte type)
|
// mask = upper 5 bits of state (indicates byte type)
|
||||||
let mask = _mm_and_si128(state, _mm_set1_epi8(0xF8u8 as i8));
|
let mask = _mm_and_si128(state, _mm_set1_epi8(0xF8u8 as i8));
|
||||||
@@ -774,9 +900,13 @@ impl SimdUtf8Decoder {
|
|||||||
// count_subs1[i] = count[i] - 1, saturating
|
// count_subs1[i] = count[i] - 1, saturating
|
||||||
let count_subs1 = _mm_subs_epu8(count, one);
|
let count_subs1 = _mm_subs_epu8(count, one);
|
||||||
// counts[i] = count[i] + count_subs1[i-1]
|
// counts[i] = count[i] + count_subs1[i-1]
|
||||||
let mut counts = _mm_add_epi8(count, _mm_srli_si128(count_subs1, 1));
|
let mut counts =
|
||||||
|
_mm_add_epi8(count, _mm_srli_si128(count_subs1, 1));
|
||||||
// counts[i] += counts_subs2[i-2] (for 3 and 4 byte sequences)
|
// counts[i] += counts_subs2[i-2] (for 3 and 4 byte sequences)
|
||||||
counts = _mm_add_epi8(counts, _mm_srli_si128(_mm_subs_epu8(counts, two), 2));
|
counts = _mm_add_epi8(
|
||||||
|
counts,
|
||||||
|
_mm_srli_si128(_mm_subs_epu8(counts, two), 2),
|
||||||
|
);
|
||||||
|
|
||||||
// Check for trailing incomplete sequence
|
// Check for trailing incomplete sequence
|
||||||
if check_for_trailing {
|
if check_for_trailing {
|
||||||
@@ -789,12 +919,19 @@ impl SimdUtf8Decoder {
|
|||||||
// We have a trailing incomplete sequence
|
// We have a trailing incomplete sequence
|
||||||
check_for_trailing = false;
|
check_for_trailing = false;
|
||||||
|
|
||||||
let last_byte = *start_of_current_chunk.add(chunk_src_sz - 1);
|
let last_byte =
|
||||||
|
*start_of_current_chunk.add(chunk_src_sz - 1);
|
||||||
if last_byte >= 0xC0 {
|
if last_byte >= 0xC0 {
|
||||||
num_trailing_bytes = 1;
|
num_trailing_bytes = 1;
|
||||||
} else if chunk_src_sz > 1 && *start_of_current_chunk.add(chunk_src_sz - 2) >= 0xE0 {
|
} else if chunk_src_sz > 1
|
||||||
|
&& *start_of_current_chunk.add(chunk_src_sz - 2)
|
||||||
|
>= 0xE0
|
||||||
|
{
|
||||||
num_trailing_bytes = 2;
|
num_trailing_bytes = 2;
|
||||||
} else if chunk_src_sz > 2 && *start_of_current_chunk.add(chunk_src_sz - 3) >= 0xF0 {
|
} else if chunk_src_sz > 2
|
||||||
|
&& *start_of_current_chunk.add(chunk_src_sz - 3)
|
||||||
|
>= 0xF0
|
||||||
|
{
|
||||||
num_trailing_bytes = 3;
|
num_trailing_bytes = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,7 +942,7 @@ impl SimdUtf8Decoder {
|
|||||||
// Fall back to scalar for trailing bytes
|
// Fall back to scalar for trailing bytes
|
||||||
let slice = std::slice::from_raw_parts(
|
let slice = std::slice::from_raw_parts(
|
||||||
start_of_current_chunk,
|
start_of_current_chunk,
|
||||||
num_trailing_bytes
|
num_trailing_bytes,
|
||||||
);
|
);
|
||||||
self.scalar_decode_all(slice, output);
|
self.scalar_decode_all(slice, output);
|
||||||
num_consumed += num_trailing_bytes;
|
num_consumed += num_trailing_bytes;
|
||||||
@@ -824,7 +961,7 @@ impl SimdUtf8Decoder {
|
|||||||
// Invalid UTF-8 - fall back to scalar
|
// Invalid UTF-8 - fall back to scalar
|
||||||
let slice = std::slice::from_raw_parts(
|
let slice = std::slice::from_raw_parts(
|
||||||
start_of_current_chunk,
|
start_of_current_chunk,
|
||||||
chunk_src_sz + num_trailing_bytes
|
chunk_src_sz + num_trailing_bytes,
|
||||||
);
|
);
|
||||||
self.scalar_decode_all(slice, output);
|
self.scalar_decode_all(slice, output);
|
||||||
num_consumed += num_trailing_bytes;
|
num_consumed += num_trailing_bytes;
|
||||||
@@ -835,53 +972,87 @@ impl SimdUtf8Decoder {
|
|||||||
let mut chunk_invalid = zero;
|
let mut chunk_invalid = zero;
|
||||||
|
|
||||||
// Validate 2-byte starters: 0xC0, 0xC1 are invalid
|
// Validate 2-byte starters: 0xC0, 0xC1 are invalid
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid,
|
chunk_invalid = _mm_or_si128(
|
||||||
_mm_and_si128(c2_start, _mm_cmplt_epi8(vec, _mm_set1_epi8(0xC2u8 as i8))));
|
chunk_invalid,
|
||||||
|
_mm_and_si128(
|
||||||
|
c2_start,
|
||||||
|
_mm_cmplt_epi8(vec, _mm_set1_epi8(0xC2u8 as i8)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Validate 4-byte starters: 0xF5+ are invalid
|
// Validate 4-byte starters: 0xF5+ are invalid
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid,
|
chunk_invalid = _mm_or_si128(
|
||||||
_mm_and_si128(f4_start, _mm_cmpgt_epi8(vec, _mm_set1_epi8(0xF4u8 as i8))));
|
chunk_invalid,
|
||||||
|
_mm_and_si128(
|
||||||
|
f4_start,
|
||||||
|
_mm_cmpgt_epi8(vec, _mm_set1_epi8(0xF4u8 as i8)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Validate continuation bytes don't have starter bytes
|
// Validate continuation bytes don't have starter bytes
|
||||||
let cont_has_starter = _mm_andnot_si128(
|
let cont_has_starter = _mm_andnot_si128(
|
||||||
_mm_cmplt_epi8(vec, _mm_set1_epi8(0xC0u8 as i8)),
|
_mm_cmplt_epi8(vec, _mm_set1_epi8(0xC0u8 as i8)),
|
||||||
_mm_cmpgt_epi8(counts, count)
|
_mm_cmpgt_epi8(counts, count),
|
||||||
);
|
);
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid, cont_has_starter);
|
chunk_invalid = _mm_or_si128(chunk_invalid, cont_has_starter);
|
||||||
|
|
||||||
// Validate E0 second bytes (must be >= 0xA0)
|
// Validate E0 second bytes (must be >= 0xA0)
|
||||||
let e0_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xE0u8 as i8));
|
let e0_starters =
|
||||||
|
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xE0u8 as i8));
|
||||||
let e0_followers = _mm_srli_si128(e0_starters, 1);
|
let e0_followers = _mm_srli_si128(e0_starters, 1);
|
||||||
let e0_invalid = _mm_and_si128(e0_followers,
|
let e0_invalid = _mm_and_si128(
|
||||||
_mm_cmplt_epi8(_mm_and_si128(e0_followers, vec), _mm_set1_epi8(0xA0u8 as i8)));
|
e0_followers,
|
||||||
|
_mm_cmplt_epi8(
|
||||||
|
_mm_and_si128(e0_followers, vec),
|
||||||
|
_mm_set1_epi8(0xA0u8 as i8),
|
||||||
|
),
|
||||||
|
);
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid, e0_invalid);
|
chunk_invalid = _mm_or_si128(chunk_invalid, e0_invalid);
|
||||||
|
|
||||||
// Validate ED second bytes (must be < 0xA0, i.e. <= 0x9F)
|
// Validate ED second bytes (must be < 0xA0, i.e. <= 0x9F)
|
||||||
let ed_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xEDu8 as i8));
|
let ed_starters =
|
||||||
|
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xEDu8 as i8));
|
||||||
let ed_followers = _mm_srli_si128(ed_starters, 1);
|
let ed_followers = _mm_srli_si128(ed_starters, 1);
|
||||||
let ed_invalid = _mm_and_si128(ed_followers,
|
let ed_invalid = _mm_and_si128(
|
||||||
_mm_cmpgt_epi8(_mm_and_si128(ed_followers, vec), _mm_set1_epi8(0x9Fu8 as i8)));
|
ed_followers,
|
||||||
|
_mm_cmpgt_epi8(
|
||||||
|
_mm_and_si128(ed_followers, vec),
|
||||||
|
_mm_set1_epi8(0x9Fu8 as i8),
|
||||||
|
),
|
||||||
|
);
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid, ed_invalid);
|
chunk_invalid = _mm_or_si128(chunk_invalid, ed_invalid);
|
||||||
|
|
||||||
// Validate F0 second bytes (must be >= 0x90)
|
// Validate F0 second bytes (must be >= 0x90)
|
||||||
let f0_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF0u8 as i8));
|
let f0_starters =
|
||||||
|
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF0u8 as i8));
|
||||||
let f0_followers = _mm_srli_si128(f0_starters, 1);
|
let f0_followers = _mm_srli_si128(f0_starters, 1);
|
||||||
let f0_invalid = _mm_and_si128(f0_followers,
|
let f0_invalid = _mm_and_si128(
|
||||||
_mm_cmplt_epi8(_mm_and_si128(f0_followers, vec), _mm_set1_epi8(0x90u8 as i8)));
|
f0_followers,
|
||||||
|
_mm_cmplt_epi8(
|
||||||
|
_mm_and_si128(f0_followers, vec),
|
||||||
|
_mm_set1_epi8(0x90u8 as i8),
|
||||||
|
),
|
||||||
|
);
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid, f0_invalid);
|
chunk_invalid = _mm_or_si128(chunk_invalid, f0_invalid);
|
||||||
|
|
||||||
// Validate F4 second bytes (must be < 0x90, i.e. <= 0x8F)
|
// Validate F4 second bytes (must be < 0x90, i.e. <= 0x8F)
|
||||||
let f4_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF4u8 as i8));
|
let f4_starters =
|
||||||
|
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF4u8 as i8));
|
||||||
let f4_followers = _mm_srli_si128(f4_starters, 1);
|
let f4_followers = _mm_srli_si128(f4_starters, 1);
|
||||||
let f4_invalid = _mm_and_si128(f4_followers,
|
let f4_invalid = _mm_and_si128(
|
||||||
_mm_cmpgt_epi8(_mm_and_si128(f4_followers, vec), _mm_set1_epi8(0x8Fu8 as i8)));
|
f4_followers,
|
||||||
|
_mm_cmpgt_epi8(
|
||||||
|
_mm_and_si128(f4_followers, vec),
|
||||||
|
_mm_set1_epi8(0x8Fu8 as i8),
|
||||||
|
),
|
||||||
|
);
|
||||||
chunk_invalid = _mm_or_si128(chunk_invalid, f4_invalid);
|
chunk_invalid = _mm_or_si128(chunk_invalid, f4_invalid);
|
||||||
|
|
||||||
// If invalid, fall back to scalar
|
// If invalid, fall back to scalar
|
||||||
if _mm_testz_si128(chunk_invalid, chunk_invalid) == 0 {
|
if _mm_testz_si128(chunk_invalid, chunk_invalid) == 0 {
|
||||||
let slice = std::slice::from_raw_parts(
|
let slice = std::slice::from_raw_parts(
|
||||||
start_of_current_chunk,
|
start_of_current_chunk,
|
||||||
chunk_src_sz + num_trailing_bytes
|
chunk_src_sz + num_trailing_bytes,
|
||||||
);
|
);
|
||||||
self.scalar_decode_all(slice, output);
|
self.scalar_decode_all(slice, output);
|
||||||
num_consumed += num_trailing_bytes;
|
num_consumed += num_trailing_bytes;
|
||||||
@@ -892,16 +1063,21 @@ impl SimdUtf8Decoder {
|
|||||||
vec = _mm_andnot_si128(mask, vec);
|
vec = _mm_andnot_si128(mask, vec);
|
||||||
|
|
||||||
// Build output vectors
|
// Build output vectors
|
||||||
let vec_non_ascii = _mm_andnot_si128(_mm_cmpeq_epi8(counts, zero), vec);
|
let vec_non_ascii =
|
||||||
|
_mm_andnot_si128(_mm_cmpeq_epi8(counts, zero), vec);
|
||||||
|
|
||||||
// output1: lowest byte of each codepoint
|
// output1: lowest byte of each codepoint
|
||||||
// For count==1 positions: OR with shifted bits from count==2 position
|
// For count==1 positions: OR with shifted bits from count==2 position
|
||||||
let count1_locs = _mm_cmpeq_epi8(counts, one);
|
let count1_locs = _mm_cmpeq_epi8(counts, one);
|
||||||
let shifted_6 = _mm_and_si128(
|
let shifted_6 = _mm_and_si128(
|
||||||
_mm_slli_epi16(_mm_srli_si128(vec_non_ascii, 1), 6),
|
_mm_slli_epi16(_mm_srli_si128(vec_non_ascii, 1), 6),
|
||||||
_mm_set1_epi8(0xC0u8 as i8)
|
_mm_set1_epi8(0xC0u8 as i8),
|
||||||
|
);
|
||||||
|
let output1 = _mm_blendv_epi8(
|
||||||
|
vec,
|
||||||
|
_mm_or_si128(vec, shifted_6),
|
||||||
|
count1_locs,
|
||||||
);
|
);
|
||||||
let output1 = _mm_blendv_epi8(vec, _mm_or_si128(vec, shifted_6), count1_locs);
|
|
||||||
|
|
||||||
// output2: middle byte (for 3 and 4 byte sequences)
|
// output2: middle byte (for 3 and 4 byte sequences)
|
||||||
let count2_locs = _mm_cmpeq_epi8(counts, two);
|
let count2_locs = _mm_cmpeq_epi8(counts, two);
|
||||||
@@ -910,7 +1086,13 @@ impl SimdUtf8Decoder {
|
|||||||
output2 = _mm_srli_epi32(output2, 2); // bits 5,4,3,2
|
output2 = _mm_srli_epi32(output2, 2); // bits 5,4,3,2
|
||||||
let shifted_4 = _mm_and_si128(
|
let shifted_4 = _mm_and_si128(
|
||||||
_mm_set1_epi8(0xF0u8 as i8),
|
_mm_set1_epi8(0xF0u8 as i8),
|
||||||
_mm_slli_epi16(_mm_srli_si128(_mm_and_si128(count3_locs, vec_non_ascii), 1), 4)
|
_mm_slli_epi16(
|
||||||
|
_mm_srli_si128(
|
||||||
|
_mm_and_si128(count3_locs, vec_non_ascii),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
4,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
output2 = _mm_or_si128(output2, shifted_4);
|
output2 = _mm_or_si128(output2, shifted_4);
|
||||||
output2 = _mm_and_si128(output2, count2_locs);
|
output2 = _mm_and_si128(output2, count2_locs);
|
||||||
@@ -921,7 +1103,13 @@ impl SimdUtf8Decoder {
|
|||||||
let mut output3 = _mm_and_si128(three, _mm_srli_epi32(vec, 4)); // bits 5,6 from count==3
|
let mut output3 = _mm_and_si128(three, _mm_srli_epi32(vec, 4)); // bits 5,6 from count==3
|
||||||
let shifted_2 = _mm_and_si128(
|
let shifted_2 = _mm_and_si128(
|
||||||
_mm_set1_epi8(0xFCu8 as i8),
|
_mm_set1_epi8(0xFCu8 as i8),
|
||||||
_mm_slli_epi16(_mm_srli_si128(_mm_and_si128(count4_locs, vec_non_ascii), 1), 2)
|
_mm_slli_epi16(
|
||||||
|
_mm_srli_si128(
|
||||||
|
_mm_and_si128(count4_locs, vec_non_ascii),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
output3 = _mm_or_si128(output3, shifted_2);
|
output3 = _mm_or_si128(output3, shifted_2);
|
||||||
output3 = _mm_and_si128(output3, count3_locs);
|
output3 = _mm_and_si128(output3, count3_locs);
|
||||||
@@ -959,7 +1147,13 @@ impl SimdUtf8Decoder {
|
|||||||
let num_codepoints = chunk_src_sz - num_discarded;
|
let num_codepoints = chunk_src_sz - num_discarded;
|
||||||
|
|
||||||
// Output unicode codepoints
|
// Output unicode codepoints
|
||||||
Self::output_unicode(output1, output2, output3, num_codepoints, output);
|
Self::output_unicode(
|
||||||
|
output1,
|
||||||
|
output2,
|
||||||
|
output3,
|
||||||
|
num_codepoints,
|
||||||
|
output,
|
||||||
|
);
|
||||||
|
|
||||||
// Handle trailing bytes
|
// Handle trailing bytes
|
||||||
if num_trailing_bytes > 0 && p < limit {
|
if num_trailing_bytes > 0 && p < limit {
|
||||||
@@ -1074,7 +1268,11 @@ impl SimdUtf8Decoder {
|
|||||||
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
|
||||||
#[target_feature(enable = "sse2", enable = "sse4.1")]
|
#[target_feature(enable = "sse2", enable = "sse4.1")]
|
||||||
#[inline]
|
#[inline]
|
||||||
unsafe fn output_plain_ascii(vec: __m128i, src_sz: usize, output: &mut Vec<u32>) {
|
unsafe fn output_plain_ascii(
|
||||||
|
vec: __m128i,
|
||||||
|
src_sz: usize,
|
||||||
|
output: &mut Vec<u32>,
|
||||||
|
) {
|
||||||
output.reserve(src_sz);
|
output.reserve(src_sz);
|
||||||
|
|
||||||
// Process 4 bytes at a time
|
// Process 4 bytes at a time
|
||||||
@@ -1102,7 +1300,7 @@ impl SimdUtf8Decoder {
|
|||||||
output2: __m128i,
|
output2: __m128i,
|
||||||
output3: __m128i,
|
output3: __m128i,
|
||||||
num_codepoints: usize,
|
num_codepoints: usize,
|
||||||
output: &mut Vec<u32>
|
output: &mut Vec<u32>,
|
||||||
) {
|
) {
|
||||||
output.reserve(num_codepoints);
|
output.reserve(num_codepoints);
|
||||||
|
|
||||||
@@ -1121,7 +1319,8 @@ impl SimdUtf8Decoder {
|
|||||||
let unpacked3 = _mm_cvtepu8_epi32(_mm_srli_si128(o3, 0));
|
let unpacked3 = _mm_cvtepu8_epi32(_mm_srli_si128(o3, 0));
|
||||||
let unpacked3 = _mm_slli_epi32(unpacked3, 16);
|
let unpacked3 = _mm_slli_epi32(unpacked3, 16);
|
||||||
|
|
||||||
let unpacked = _mm_or_si128(_mm_or_si128(unpacked1, unpacked2), unpacked3);
|
let unpacked =
|
||||||
|
_mm_or_si128(_mm_or_si128(unpacked1, unpacked2), unpacked3);
|
||||||
|
|
||||||
let to_write = remaining.min(4);
|
let to_write = remaining.min(4);
|
||||||
let mut buf = [0u32; 4];
|
let mut buf = [0u32; 4];
|
||||||
@@ -1136,7 +1335,11 @@ impl SimdUtf8Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scalar decode until state is ACCEPT.
|
/// Scalar decode until state is ACCEPT.
|
||||||
fn scalar_decode_to_accept(&mut self, src: &[u8], output: &mut Vec<u32>) -> usize {
|
fn scalar_decode_to_accept(
|
||||||
|
&mut self,
|
||||||
|
src: &[u8],
|
||||||
|
output: &mut Vec<u32>,
|
||||||
|
) -> usize {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
while pos < src.len() && self.state.cur != UTF8_ACCEPT {
|
while pos < src.len() && self.state.cur != UTF8_ACCEPT {
|
||||||
let byte = src[pos];
|
let byte = src[pos];
|
||||||
@@ -1147,7 +1350,11 @@ impl SimdUtf8Decoder {
|
|||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
self.state.prev = self.state.cur;
|
self.state.prev = self.state.cur;
|
||||||
match decode_utf8_byte(&mut self.state.cur, &mut self.state.codep, byte) {
|
match decode_utf8_byte(
|
||||||
|
&mut self.state.cur,
|
||||||
|
&mut self.state.codep,
|
||||||
|
byte,
|
||||||
|
) {
|
||||||
UTF8_ACCEPT => output.push(self.state.codep),
|
UTF8_ACCEPT => output.push(self.state.codep),
|
||||||
UTF8_REJECT => {
|
UTF8_REJECT => {
|
||||||
output.push(0xFFFD);
|
output.push(0xFFFD);
|
||||||
@@ -1164,7 +1371,11 @@ impl SimdUtf8Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scalar decode all bytes.
|
/// Scalar decode all bytes.
|
||||||
fn scalar_decode_all(&mut self, src: &[u8], output: &mut Vec<u32>) -> usize {
|
fn scalar_decode_all(
|
||||||
|
&mut self,
|
||||||
|
src: &[u8],
|
||||||
|
output: &mut Vec<u32>,
|
||||||
|
) -> usize {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
while pos < src.len() {
|
while pos < src.len() {
|
||||||
let byte = src[pos];
|
let byte = src[pos];
|
||||||
@@ -1177,7 +1388,11 @@ impl SimdUtf8Decoder {
|
|||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
self.state.prev = self.state.cur;
|
self.state.prev = self.state.cur;
|
||||||
match decode_utf8_byte(&mut self.state.cur, &mut self.state.codep, byte) {
|
match decode_utf8_byte(
|
||||||
|
&mut self.state.cur,
|
||||||
|
&mut self.state.codep,
|
||||||
|
byte,
|
||||||
|
) {
|
||||||
UTF8_ACCEPT => output.push(self.state.codep),
|
UTF8_ACCEPT => output.push(self.state.codep),
|
||||||
UTF8_REJECT => {
|
UTF8_REJECT => {
|
||||||
output.push(0xFFFD);
|
output.push(0xFFFD);
|
||||||
@@ -1222,7 +1437,8 @@ mod tests {
|
|||||||
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
||||||
assert_eq!(consumed, 13);
|
assert_eq!(consumed, 13);
|
||||||
assert!(!found_esc);
|
assert!(!found_esc);
|
||||||
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
let chars: Vec<char> =
|
||||||
|
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
||||||
assert_eq!(chars.iter().collect::<String>(), "Hello, World!");
|
assert_eq!(chars.iter().collect::<String>(), "Hello, World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1234,7 +1450,8 @@ mod tests {
|
|||||||
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
||||||
assert_eq!(consumed, 6); // "Hello" + ESC
|
assert_eq!(consumed, 6); // "Hello" + ESC
|
||||||
assert!(found_esc);
|
assert!(found_esc);
|
||||||
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
let chars: Vec<char> =
|
||||||
|
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
||||||
assert_eq!(chars.iter().collect::<String>(), "Hello");
|
assert_eq!(chars.iter().collect::<String>(), "Hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1246,7 +1463,8 @@ mod tests {
|
|||||||
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
||||||
assert_eq!(consumed, 5); // c, a, f, é (2 bytes)
|
assert_eq!(consumed, 5); // c, a, f, é (2 bytes)
|
||||||
assert!(!found_esc);
|
assert!(!found_esc);
|
||||||
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
let chars: Vec<char> =
|
||||||
|
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
||||||
assert_eq!(chars.iter().collect::<String>(), "café");
|
assert_eq!(chars.iter().collect::<String>(), "café");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1258,7 +1476,8 @@ mod tests {
|
|||||||
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
||||||
assert_eq!(consumed, 9); // 3 chars * 3 bytes
|
assert_eq!(consumed, 9); // 3 chars * 3 bytes
|
||||||
assert!(!found_esc);
|
assert!(!found_esc);
|
||||||
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
let chars: Vec<char> =
|
||||||
|
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
||||||
assert_eq!(chars.iter().collect::<String>(), "日本語");
|
assert_eq!(chars.iter().collect::<String>(), "日本語");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1270,7 +1489,8 @@ mod tests {
|
|||||||
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
|
||||||
assert_eq!(consumed, 8); // 2 chars * 4 bytes
|
assert_eq!(consumed, 8); // 2 chars * 4 bytes
|
||||||
assert!(!found_esc);
|
assert!(!found_esc);
|
||||||
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
let chars: Vec<char> =
|
||||||
|
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
|
||||||
assert_eq!(chars.iter().collect::<String>(), "🎉🚀");
|
assert_eq!(chars.iter().collect::<String>(), "🎉🚀");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -113,7 +113,10 @@ impl StatuslineSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add multiple components to this section.
|
/// Add multiple components to this section.
|
||||||
pub fn with_components(mut self, components: Vec<StatuslineComponent>) -> Self {
|
pub fn with_components(
|
||||||
|
mut self,
|
||||||
|
components: Vec<StatuslineComponent>,
|
||||||
|
) -> Self {
|
||||||
self.components = components;
|
self.components = components;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-37
@@ -1,7 +1,7 @@
|
|||||||
//! Terminal state management and escape sequence handling.
|
//! Terminal state management and escape sequence handling.
|
||||||
|
|
||||||
use crate::graphics::{GraphicsCommand, ImageStorage};
|
use crate::graphics::{GraphicsCommand, ImageStorage};
|
||||||
use crate::keyboard::{query_response, KeyboardState};
|
use crate::keyboard::{KeyboardState, query_response};
|
||||||
use crate::vt_parser::{CsiParams, Handler};
|
use crate::vt_parser::{CsiParams, Handler};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
@@ -287,8 +287,7 @@ struct SavedCursor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Alternate screen buffer state.
|
/// Alternate screen buffer state.
|
||||||
#[derive(Clone)]
|
pub struct AlternateScreen {
|
||||||
struct AlternateScreen {
|
|
||||||
grid: Vec<Vec<Cell>>,
|
grid: Vec<Vec<Cell>>,
|
||||||
line_map: Vec<usize>,
|
line_map: Vec<usize>,
|
||||||
cursor_col: usize,
|
cursor_col: usize,
|
||||||
@@ -296,6 +295,7 @@ struct AlternateScreen {
|
|||||||
saved_cursor: SavedCursor,
|
saved_cursor: SavedCursor,
|
||||||
scroll_top: usize,
|
scroll_top: usize,
|
||||||
scroll_bottom: usize,
|
scroll_bottom: usize,
|
||||||
|
pub image_storage: ImageStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kitty-style ring buffer for scrollback history.
|
/// Kitty-style ring buffer for scrollback history.
|
||||||
@@ -418,7 +418,7 @@ pub struct Terminal {
|
|||||||
pub grid: Vec<Vec<Cell>>,
|
pub grid: Vec<Vec<Cell>>,
|
||||||
/// Maps visual row index to actual grid row index.
|
/// Maps visual row index to actual grid row index.
|
||||||
/// This allows O(1) scrolling by rotating indices instead of moving cells.
|
/// This allows O(1) scrolling by rotating indices instead of moving cells.
|
||||||
line_map: Vec<usize>,
|
pub line_map: Vec<usize>,
|
||||||
/// Number of columns.
|
/// Number of columns.
|
||||||
pub cols: usize,
|
pub cols: usize,
|
||||||
/// Number of rows.
|
/// Number of rows.
|
||||||
@@ -472,7 +472,7 @@ pub struct Terminal {
|
|||||||
/// Saved cursor state (DECSC/DECRC).
|
/// Saved cursor state (DECSC/DECRC).
|
||||||
saved_cursor: SavedCursor,
|
saved_cursor: SavedCursor,
|
||||||
/// Alternate screen buffer (for fullscreen apps like vim, less).
|
/// Alternate screen buffer (for fullscreen apps like vim, less).
|
||||||
alternate_screen: Option<AlternateScreen>,
|
pub alternate_screen: Option<AlternateScreen>,
|
||||||
/// Whether we're currently using the alternate screen.
|
/// Whether we're currently using the alternate screen.
|
||||||
pub using_alternate_screen: bool,
|
pub using_alternate_screen: bool,
|
||||||
/// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [.
|
/// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [.
|
||||||
@@ -584,7 +584,10 @@ impl Terminal {
|
|||||||
/// Check if any line is dirty.
|
/// Check if any line is dirty.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_any_dirty_line(&self) -> bool {
|
pub fn has_any_dirty_line(&self) -> bool {
|
||||||
self.dirty_lines[0] != 0 || self.dirty_lines[1] != 0 || self.dirty_lines[2] != 0 || self.dirty_lines[3] != 0
|
self.dirty_lines[0] != 0
|
||||||
|
|| self.dirty_lines[1] != 0
|
||||||
|
|| self.dirty_lines[2] != 0
|
||||||
|
|| self.dirty_lines[3] != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all dirty line flags.
|
/// Clear all dirty line flags.
|
||||||
@@ -775,16 +778,19 @@ impl Terminal {
|
|||||||
return; // Already in alternate screen
|
return; // Already in alternate screen
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save main screen state
|
// Create alternate screen if it doesn't exist, otherwise reuse it
|
||||||
self.alternate_screen = Some(AlternateScreen {
|
if self.alternate_screen.is_none() {
|
||||||
grid: self.grid.clone(),
|
self.alternate_screen = Some(AlternateScreen {
|
||||||
line_map: self.line_map.clone(),
|
grid: self.grid.clone(),
|
||||||
cursor_col: self.cursor_col,
|
line_map: self.line_map.clone(),
|
||||||
cursor_row: self.cursor_row,
|
cursor_col: self.cursor_col,
|
||||||
saved_cursor: self.saved_cursor.clone(),
|
cursor_row: self.cursor_row,
|
||||||
scroll_top: self.scroll_top,
|
saved_cursor: self.saved_cursor.clone(),
|
||||||
scroll_bottom: self.scroll_bottom,
|
scroll_top: self.scroll_top,
|
||||||
});
|
scroll_bottom: self.scroll_bottom,
|
||||||
|
image_storage: ImageStorage::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the screen for alternate buffer
|
// Clear the screen for alternate buffer
|
||||||
self.grid = vec![vec![Cell::default(); self.cols]; self.rows];
|
self.grid = vec![vec![Cell::default(); self.cols]; self.rows];
|
||||||
@@ -801,8 +807,14 @@ impl Terminal {
|
|||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Entered alternate screen buffer: rows={}, cols={}, scroll_region={}-{}, dirty_lines={:016x}{:016x}{:016x}{:016x}",
|
"Entered alternate screen buffer: rows={}, cols={}, scroll_region={}-{}, dirty_lines={:016x}{:016x}{:016x}{:016x}",
|
||||||
self.rows, self.cols, self.scroll_top, self.scroll_bottom,
|
self.rows,
|
||||||
self.dirty_lines[3], self.dirty_lines[2], self.dirty_lines[1], self.dirty_lines[0]
|
self.cols,
|
||||||
|
self.scroll_top,
|
||||||
|
self.scroll_bottom,
|
||||||
|
self.dirty_lines[3],
|
||||||
|
self.dirty_lines[2],
|
||||||
|
self.dirty_lines[1],
|
||||||
|
self.dirty_lines[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,10 +824,10 @@ impl Terminal {
|
|||||||
return; // Not in alternate screen
|
return; // Not in alternate screen
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(saved) = self.alternate_screen.take() {
|
if let Some(saved) = self.alternate_screen.as_ref() {
|
||||||
self.grid = saved.grid;
|
self.grid = saved.grid.clone();
|
||||||
self.line_map = saved.line_map;
|
self.line_map = saved.line_map.clone();
|
||||||
self.saved_cursor = saved.saved_cursor;
|
self.saved_cursor = saved.saved_cursor.clone();
|
||||||
self.scroll_top = saved.scroll_top;
|
self.scroll_top = saved.scroll_top;
|
||||||
self.scroll_bottom = saved.scroll_bottom;
|
self.scroll_bottom = saved.scroll_bottom;
|
||||||
// Clamp cursor positions to current grid dimensions (defensive)
|
// Clamp cursor positions to current grid dimensions (defensive)
|
||||||
@@ -823,9 +835,11 @@ impl Terminal {
|
|||||||
self.cursor_row = saved.cursor_row.min(self.rows.saturating_sub(1));
|
self.cursor_row = saved.cursor_row.min(self.rows.saturating_sub(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wipe alternate screen and its image storage to reclaim memory
|
||||||
|
self.alternate_screen = None;
|
||||||
self.using_alternate_screen = false;
|
self.using_alternate_screen = false;
|
||||||
self.mark_all_lines_dirty();
|
self.mark_all_lines_dirty();
|
||||||
log::debug!("Left alternate screen buffer");
|
log::debug!("Left alternate screen buffer and cleared its storage");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls the scroll region up by n lines.
|
/// Scrolls the scroll region up by n lines.
|
||||||
@@ -852,6 +866,9 @@ impl Terminal {
|
|||||||
{
|
{
|
||||||
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
|
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
|
||||||
// If buffer is full, this overwrites the oldest line (perfect for our swap)
|
// If buffer is full, this overwrites the oldest line (perfect for our swap)
|
||||||
|
if self.scrollback.is_full() {
|
||||||
|
self.image_storage.shift_placements(-1);
|
||||||
|
}
|
||||||
let cols = self.cols;
|
let cols = self.cols;
|
||||||
let dest = self.scrollback.push(cols);
|
let dest = self.scrollback.push(cols);
|
||||||
// Swap grid row content into scrollback slot
|
// Swap grid row content into scrollback slot
|
||||||
@@ -859,6 +876,10 @@ impl Terminal {
|
|||||||
std::mem::swap(&mut self.grid[recycled_grid_row], dest);
|
std::mem::swap(&mut self.grid[recycled_grid_row], dest);
|
||||||
// Clear the grid row (now contains old scrollback data or empty)
|
// Clear the grid row (now contains old scrollback data or empty)
|
||||||
self.clear_grid_row(recycled_grid_row);
|
self.clear_grid_row(recycled_grid_row);
|
||||||
|
if self.scroll_offset > 0 {
|
||||||
|
self.scroll_offset =
|
||||||
|
(self.scroll_offset + 1).min(self.scrollback.capacity);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not saving to scrollback - just clear the line
|
// Not saving to scrollback - just clear the line
|
||||||
self.clear_grid_row(recycled_grid_row);
|
self.clear_grid_row(recycled_grid_row);
|
||||||
@@ -1293,6 +1314,9 @@ impl Terminal {
|
|||||||
for visual_row in 0..self.rows {
|
for visual_row in 0..self.rows {
|
||||||
let grid_row = self.line_map[visual_row];
|
let grid_row = self.line_map[visual_row];
|
||||||
// Get a slot in the ring buffer and swap content into it
|
// Get a slot in the ring buffer and swap content into it
|
||||||
|
if self.scrollback.is_full() {
|
||||||
|
self.image_storage.shift_placements(-1);
|
||||||
|
}
|
||||||
let cols = self.cols;
|
let cols = self.cols;
|
||||||
let dest = self.scrollback.push(cols);
|
let dest = self.scrollback.push(cols);
|
||||||
std::mem::swap(&mut self.grid[grid_row], dest);
|
std::mem::swap(&mut self.grid[grid_row], dest);
|
||||||
@@ -1328,8 +1352,12 @@ impl Handler for Terminal {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&c| char::from_u32(c))
|
.filter_map(|&c| char::from_u32(c))
|
||||||
.collect();
|
.collect();
|
||||||
log::error!("DEBUG CSI LEAK: text handler received CSI-like content: {:?} at ({}, {})",
|
log::error!(
|
||||||
text, self.cursor_col, self.cursor_row);
|
"DEBUG CSI LEAK: text handler received CSI-like content: {:?} at ({}, {})",
|
||||||
|
text,
|
||||||
|
self.cursor_col,
|
||||||
|
self.cursor_row
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,7 +1700,9 @@ impl Handler for Terminal {
|
|||||||
b'1' => {
|
b'1' => {
|
||||||
// Start pending mode (pause rendering)
|
// Start pending mode (pause rendering)
|
||||||
if self.synchronized_output {
|
if self.synchronized_output {
|
||||||
log::warn!("Pending mode start requested while already in pending mode");
|
log::warn!(
|
||||||
|
"Pending mode start requested while already in pending mode"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.synchronized_output = true;
|
self.synchronized_output = true;
|
||||||
log::trace!("DCS pending mode started (=1s)");
|
log::trace!("DCS pending mode started (=1s)");
|
||||||
@@ -1680,7 +1710,9 @@ impl Handler for Terminal {
|
|||||||
b'2' => {
|
b'2' => {
|
||||||
// Stop pending mode (resume rendering)
|
// Stop pending mode (resume rendering)
|
||||||
if !self.synchronized_output {
|
if !self.synchronized_output {
|
||||||
log::warn!("Pending mode stop requested while not in pending mode");
|
log::warn!(
|
||||||
|
"Pending mode stop requested while not in pending mode"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.synchronized_output = false;
|
self.synchronized_output = false;
|
||||||
self.dirty = true; // Force a redraw
|
self.dirty = true; // Force a redraw
|
||||||
@@ -1736,7 +1768,7 @@ impl Handler for Terminal {
|
|||||||
"CSI C: cursor forward {} from col {} to {}",
|
"CSI C: cursor forward {} from col {} to {}",
|
||||||
n,
|
n,
|
||||||
old_col,
|
old_col,
|
||||||
self.cursor_col
|
self.cursor_col,
|
||||||
);
|
);
|
||||||
self.mark_line_dirty(self.cursor_row);
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
@@ -1774,7 +1806,7 @@ impl Handler for Terminal {
|
|||||||
log::trace!(
|
log::trace!(
|
||||||
"CSI G: cursor to col {} (was {})",
|
"CSI G: cursor to col {} (was {})",
|
||||||
self.cursor_col,
|
self.cursor_col,
|
||||||
old_col
|
old_col,
|
||||||
);
|
);
|
||||||
self.mark_line_dirty(self.cursor_row);
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
@@ -1790,6 +1822,13 @@ impl Handler for Terminal {
|
|||||||
self.cursor_row = (row - 1).min(self.rows - 1);
|
self.cursor_row = (row - 1).min(self.rows - 1);
|
||||||
}
|
}
|
||||||
self.cursor_col = (col - 1).min(self.cols - 1);
|
self.cursor_col = (col - 1).min(self.cols - 1);
|
||||||
|
log::debug!(
|
||||||
|
"CSI H/f: cursor to row {}, col {} (was {}, {})",
|
||||||
|
self.cursor_row,
|
||||||
|
self.cursor_col,
|
||||||
|
self.cursor_row,
|
||||||
|
self.cursor_col
|
||||||
|
);
|
||||||
self.mark_line_dirty(self.cursor_row);
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
// Erase in Display
|
// Erase in Display
|
||||||
@@ -2089,7 +2128,10 @@ impl Handler for Terminal {
|
|||||||
_ => {
|
_ => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Unhandled CSI: action='{}' primary={} secondary={} params={:?}",
|
"Unhandled CSI: action='{}' primary={} secondary={} params={:?}",
|
||||||
action, primary, secondary, ¶ms.params[..params.num_params]
|
action,
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
¶ms.params[..params.num_params]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2241,8 +2283,9 @@ impl Handler for Terminal {
|
|||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
wide_continuation: false,
|
wide_continuation: false,
|
||||||
wrapped: false,
|
wrapped: false,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mark_line_dirty(visual_row);
|
self.mark_line_dirty(visual_row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2721,17 +2764,45 @@ impl Terminal {
|
|||||||
|
|
||||||
// Convert cursor_row to absolute row (accounting for scrollback)
|
// Convert cursor_row to absolute row (accounting for scrollback)
|
||||||
// This allows images to scroll with terminal content
|
// This allows images to scroll with terminal content
|
||||||
let absolute_row = self.scrollback.len() + self.cursor_row;
|
let absolute_row = if self.using_alternate_screen {
|
||||||
|
self.cursor_row
|
||||||
|
} else {
|
||||||
|
self.scrollback.len() + self.cursor_row
|
||||||
|
};
|
||||||
|
|
||||||
// Process the command
|
log::debug!(
|
||||||
let (response, placement_result) =
|
"Routing image command to {}: cursor_col={}, absolute_row={}, using_alt={}",
|
||||||
|
if self.using_alternate_screen {
|
||||||
|
"alternate"
|
||||||
|
} else {
|
||||||
|
"main"
|
||||||
|
},
|
||||||
|
self.cursor_col,
|
||||||
|
absolute_row,
|
||||||
|
self.using_alternate_screen
|
||||||
|
);
|
||||||
|
let (response, placement_result) = if self.using_alternate_screen {
|
||||||
|
if let Some(ref mut alt) = self.alternate_screen {
|
||||||
|
alt.image_storage.process_command(
|
||||||
|
cmd,
|
||||||
|
self.cursor_col,
|
||||||
|
absolute_row,
|
||||||
|
self.cell_width,
|
||||||
|
self.cell_height,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// This should not happen if using_alternate_screen is true
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
self.image_storage.process_command(
|
self.image_storage.process_command(
|
||||||
cmd,
|
cmd,
|
||||||
self.cursor_col,
|
self.cursor_col,
|
||||||
absolute_row,
|
absolute_row,
|
||||||
self.cell_width,
|
self.cell_width,
|
||||||
self.cell_height,
|
self.cell_height,
|
||||||
);
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Queue the response to send back to the application
|
// Queue the response to send back to the application
|
||||||
if let Some(resp) = response {
|
if let Some(resp) = response {
|
||||||
@@ -2744,10 +2815,11 @@ impl Terminal {
|
|||||||
// by the number of rows in the image placement rectangle."
|
// by the number of rows in the image placement rectangle."
|
||||||
// However, if C=1 was specified, don't move the cursor.
|
// However, if C=1 was specified, don't move the cursor.
|
||||||
if let Some(placement) = placement_result {
|
if let Some(placement) = placement_result {
|
||||||
|
self.dirty = true;
|
||||||
if !placement.suppress_cursor_move
|
if !placement.suppress_cursor_move
|
||||||
&& !placement.virtual_placement
|
&& !placement.virtual_placement
|
||||||
{
|
{
|
||||||
// Move cursor to the right and down by the image dimensions
|
// Move cursor to the right and and down by the image dimensions
|
||||||
self.cursor_col += placement.cols;
|
self.cursor_col += placement.cols;
|
||||||
let new_row = self.cursor_row + placement.rows;
|
let new_row = self.cursor_row + placement.rows;
|
||||||
if new_row >= self.rows {
|
if new_row >= self.rows {
|
||||||
@@ -2762,7 +2834,10 @@ impl Terminal {
|
|||||||
// cursor movement logic (wrapping/scrolling) if applicable.
|
// cursor movement logic (wrapping/scrolling) if applicable.
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
|
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
|
||||||
self.cursor_col, self.cursor_row, placement.cols, placement.rows
|
self.cursor_col,
|
||||||
|
self.cursor_row,
|
||||||
|
placement.cols,
|
||||||
|
placement.rows
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -550,8 +550,11 @@ impl SharedParser {
|
|||||||
}
|
}
|
||||||
} else if buffer_was_ever_full {
|
} else if buffer_was_ever_full {
|
||||||
// Buffer was full but nothing consumed - stuck in partial sequence?
|
// Buffer was full but nothing consumed - stuck in partial sequence?
|
||||||
log::warn!("[PARSE] Buffer was full but read_consumed=0! read_pos={} read_sz={}",
|
log::warn!(
|
||||||
state.read_pos, state.read_sz);
|
"[PARSE] Buffer was full but read_consumed=0! read_pos={} read_sz={}",
|
||||||
|
state.read_pos,
|
||||||
|
state.read_sz
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|||||||
+26
-6
@@ -33,7 +33,10 @@ impl Handler for DummyHandler {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_osc_leak_byte_by_byte() {
|
fn test_osc_leak_byte_by_byte() {
|
||||||
let parser = SharedParser::new();
|
let parser = SharedParser::new();
|
||||||
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() };
|
let mut handler = DummyHandler {
|
||||||
|
text: String::new(),
|
||||||
|
osc_calls: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
let data = b"\x1b]4;1;#769E00\x1b\\\x1b]4;2;#93DE88\x1b\\";
|
let data = b"\x1b]4;1;#769E00\x1b\\\x1b]4;2;#93DE88\x1b\\";
|
||||||
|
|
||||||
@@ -53,14 +56,24 @@ fn test_osc_leak_byte_by_byte() {
|
|||||||
println!(" OSC: {:?}", std::str::from_utf8(call).unwrap());
|
println!(" OSC: {:?}", std::str::from_utf8(call).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!");
|
assert_eq!(
|
||||||
assert_eq!(handler.osc_calls.len(), 2, "Should have parsed exactly two OSC calls");
|
handler.text, "",
|
||||||
|
"Text should be empty, but leaked escape sequence bytes!"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
handler.osc_calls.len(),
|
||||||
|
2,
|
||||||
|
"Should have parsed exactly two OSC calls"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_csi_aborted_by_osc() {
|
fn test_csi_aborted_by_osc() {
|
||||||
let parser = SharedParser::new();
|
let parser = SharedParser::new();
|
||||||
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() };
|
let mut handler = DummyHandler {
|
||||||
|
text: String::new(),
|
||||||
|
osc_calls: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
// An incomplete CSI sequence aborted by an OSC sequence
|
// An incomplete CSI sequence aborted by an OSC sequence
|
||||||
let data = b"\x1b[38;2;255;0;0\x1b]4;1;#769E00\x1b\\";
|
let data = b"\x1b[38;2;255;0;0\x1b]4;1;#769E00\x1b\\";
|
||||||
@@ -74,6 +87,13 @@ fn test_csi_aborted_by_osc() {
|
|||||||
|
|
||||||
while parser.run_parse_pass(&mut handler) {}
|
while parser.run_parse_pass(&mut handler) {}
|
||||||
|
|
||||||
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!");
|
assert_eq!(
|
||||||
assert_eq!(handler.osc_calls.len(), 1, "Should have parsed the OSC sequence even after aborting CSI");
|
handler.text, "",
|
||||||
|
"Text should be empty, but leaked escape sequence bytes!"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
handler.osc_calls.len(),
|
||||||
|
1,
|
||||||
|
"Should have parsed the OSC sequence even after aborting CSI"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user