fix rendering bug

This commit is contained in:
2026-06-04 14:05:45 +02:00
parent 7f4249dde9
commit 3164873a66
5 changed files with 487 additions and 466 deletions
-3
View File
@@ -387,7 +387,6 @@ impl Config {
}; };
if !config_path.exists() { if !config_path.exists() {
log::info!("No config file found at {:?}, creating with defaults", config_path);
let default_config = Self::default(); let default_config = Self::default();
if let Err(e) = default_config.save() { if let Err(e) = default_config.save() {
log::warn!("Failed to write default config: {}", e); log::warn!("Failed to write default config: {}", e);
@@ -398,7 +397,6 @@ 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) => {
log::info!("Loaded config from {:?}", config_path);
config config
} }
Err(e) => { Err(e) => {
@@ -431,7 +429,6 @@ impl Config {
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
fs::write(&config_path, json)?; fs::write(&config_path, json)?;
log::info!("Saved config to {:?}", config_path);
Ok(()) Ok(())
} }
} }
+2 -19
View File
@@ -214,19 +214,6 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
// 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);
log::info!("Font family '{}' resolved to:", family);
for (i, path) in paths.iter().enumerate() {
let style = match i {
0 => "Regular",
1 => "Bold",
2 => "Italic",
3 => "BoldItalic",
_ => "Unknown",
};
if let Some(p) = path {
log::info!(" {}: {:?}", style, p);
}
}
// Load the regular font (required) // Load the regular font (required)
if let Some(regular_path) = &paths[0] { if let Some(regular_path) = &paths[0] {
@@ -277,11 +264,7 @@ 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)),
]; ];
log::info!("Loaded font from fallback paths:");
log::info!(" Regular: {}", regular);
if variants[1].is_some() { log::info!(" Bold: {}", bold); }
if variants[2].is_some() { log::info!(" Italic: {}", italic); }
if variants[3].is_some() { log::info!(" BoldItalic: {}", bold_italic); }
return (font_data, primary_font, variants); return (font_data, primary_font, variants);
} }
@@ -293,7 +276,7 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
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];
log::info!("Loaded NotoSansMono as fallback");
return (font_data, primary_font, variants); return (font_data, primary_font, variants);
} }
+396 -229
View File
@@ -219,6 +219,8 @@ impl Pane {
/// Geometry of a pane in pixels. /// Geometry of a pane in pixels.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct PaneGeometry { struct PaneGeometry {
/// Pane ID.
pane_id: PaneId,
/// Left edge in pixels. /// Left edge in pixels.
x: f32, x: f32,
/// Top edge in pixels. /// Top edge in pixels.
@@ -260,6 +262,7 @@ impl SplitNode {
SplitNode::Leaf { SplitNode::Leaf {
pane_id, pane_id,
geometry: PaneGeometry { geometry: PaneGeometry {
pane_id,
x: 0.0, x: 0.0,
y: 0.0, y: 0.0,
width: 0.0, width: 0.0,
@@ -295,13 +298,14 @@ impl SplitNode {
_border_width: f32, _border_width: f32,
) -> (f32, f32) { ) -> (f32, f32) {
match self { match self {
SplitNode::Leaf { geometry, .. } => { SplitNode::Leaf { pane_id, geometry } => {
// Calculate how many cells fit // Calculate how many cells fit
let cols = (width / cell_width).floor() as usize; let cols = (width / cell_width).floor() as usize;
let rows = (height / cell_height).floor() as usize; let rows = (height / cell_height).floor() as usize;
// Store the full allocated dimensions (not just cell-aligned) // Store the full allocated dimensions (not just cell-aligned)
// This ensures edge glow and pane dimming cover the full pane area // This ensures edge glow and pane dimming cover the full pane area
*geometry = PaneGeometry { *geometry = PaneGeometry {
pane_id: *pane_id,
x, x,
y, y,
width, // Full allocated width width, // Full allocated width
@@ -436,6 +440,30 @@ impl SplitNode {
} }
} }
/// Find the geometry of the pane that contains the given pixel position.
fn find_pane_at_pixel(&self, x: f64, y: f64) -> Option<PaneGeometry> {
match self {
SplitNode::Leaf { geometry, .. } => {
if x >= geometry.x as f64
&& x < (geometry.x + geometry.width) as f64
&& y >= geometry.y as f64
&& y < (geometry.y + geometry.height) as f64
{
Some(*geometry)
} else {
None
}
}
SplitNode::Split {
first, second, ..
} => {
first
.find_pane_at_pixel(x, y)
.or_else(|| second.find_pane_at_pixel(x, y))
}
}
}
/// Find a neighbor pane in the given direction. /// Find a neighbor pane in the given direction.
/// Returns the pane ID of the neighbor, if any. /// Returns the pane ID of the neighbor, if any.
fn find_neighbor( fn find_neighbor(
@@ -833,6 +861,17 @@ impl Tab {
fn child_exited(&mut self) -> bool { fn child_exited(&mut self) -> bool {
self.check_exited_panes() self.check_exited_panes()
} }
/// Focus a specific pane by ID. Returns true if focus changed.
fn focus_pane_by_id(&mut self, pane_id: PaneId) -> bool {
if self.panes.contains_key(&pane_id) {
let old = self.active_pane;
self.active_pane = pane_id;
old != pane_id
} else {
false
}
}
} }
/// PID file location for single-instance support. /// PID file location for single-instance support.
@@ -854,7 +893,6 @@ fn signal_existing_instance() -> bool {
if alive { if alive {
// Send SIGUSR1 to show window // Send SIGUSR1 to show window
log::info!("Signaling existing instance (PID {})", pid);
unsafe { libc::kill(pid, libc::SIGUSR1) }; unsafe { libc::kill(pid, libc::SIGUSR1) };
return true; return true;
} else { } else {
@@ -1416,13 +1454,8 @@ struct App {
impl App { impl App {
fn new() -> Self { fn new() -> Self {
let config = Config::load(); let config = Config::load();
log::info!("Config: font_size={}", config.font_size);
let action_map = config.keybindings.build_action_map(); let action_map = config.keybindings.build_action_map();
log::info!("Action map built with {} bindings:", action_map.len());
for (key, action) in &action_map {
log::info!(" {:?} => {:?}", key, action);
}
Self { Self {
window: None, window: None,
@@ -1463,7 +1496,6 @@ impl App {
/// Reload configuration from disk and apply changes. /// Reload configuration from disk and apply changes.
fn reload_config(&mut self) { fn reload_config(&mut self) {
log::info!("Reloading configuration...");
let new_config = Config::load(); let new_config = Config::load();
// Check what changed and apply updates // Check what changed and apply updates
@@ -1475,6 +1507,7 @@ impl App {
> 0.01; > 0.01;
let tab_bar_changed = let tab_bar_changed =
new_config.tab_bar_position != self.config.tab_bar_position; new_config.tab_bar_position != self.config.tab_bar_position;
let new_font_size = new_config.font_size;
// Update the config // Update the config
self.config = new_config; self.config = new_config;
@@ -1486,23 +1519,14 @@ impl App {
if let Some(renderer) = &mut self.renderer { if let Some(renderer) = &mut self.renderer {
if opacity_changed { if opacity_changed {
renderer.set_background_opacity(self.config.background_opacity); renderer.set_background_opacity(self.config.background_opacity);
log::info!(
"Updated background opacity to {}",
self.config.background_opacity
);
} }
if tab_bar_changed { if tab_bar_changed {
renderer.set_tab_bar_position(self.config.tab_bar_position); renderer.set_tab_bar_position(self.config.tab_bar_position);
log::info!(
"Updated tab bar position to {:?}",
self.config.tab_bar_position
);
} }
if font_size_changed { if font_size_changed {
renderer.set_font_size(self.config.font_size); self.config.font_size = new_font_size;
log::info!("Updated font size to {}", self.config.font_size);
// Font size change requires resize to recalculate cell dimensions // Font size change requires resize to recalculate cell dimensions
self.resize_all_panes(); self.resize_all_panes();
} }
@@ -1510,8 +1534,6 @@ impl App {
// Request redraw to apply visual changes // Request redraw to apply visual changes
self.request_redraw(); self.request_redraw();
log::info!("Configuration reloaded successfully");
} }
/// Request a window redraw if window is available. /// Request a window redraw if window is available.
@@ -1525,8 +1547,6 @@ impl App {
/// Create a new tab and start its I/O thread. /// Create a new tab and start its I/O thread.
/// Returns the index of the new tab. /// Returns the index of the new tab.
fn create_tab(&mut self, cols: usize, rows: usize) -> Option<usize> { fn create_tab(&mut self, cols: usize, rows: usize) -> Option<usize> {
log::info!("Creating new tab with {}x{} terminal", cols, rows);
match Tab::new(cols, rows, self.config.scrollback_lines) { match Tab::new(cols, rows, self.config.scrollback_lines) {
Ok(tab) => { Ok(tab) => {
let tab_idx = self.tabs.len(); let tab_idx = self.tabs.len();
@@ -1539,11 +1559,6 @@ impl App {
self.tabs.push(tab); self.tabs.push(tab);
self.active_tab = tab_idx; self.active_tab = tab_idx;
log::info!(
"Tab {} created (total: {})",
tab_idx,
self.tabs.len()
);
Some(tab_idx) Some(tab_idx)
} }
Err(e) => { Err(e) => {
@@ -1749,17 +1764,6 @@ impl App {
} }
} }
#[cfg(feature = "render_timing")]
{
let elapsed = io_start.elapsed();
log::info!("[IO-{}] Thread exiting: loops={} total_bytes={} elapsed={:?} throughput={:.1} MB/s",
pane_id.0, loop_count, total_bytes_read, elapsed,
total_bytes_read as f64 / elapsed.as_secs_f64() / 1_000_000.0);
}
#[cfg(not(feature = "render_timing"))]
let _ = (io_start, loop_count, total_bytes_read); // silence unused warnings
log::debug!("PTY I/O thread for pane {} exiting", pane_id.0); log::debug!("PTY I/O thread for pane {} exiting", pane_id.0);
}) })
.expect("Failed to spawn PTY I/O thread"); .expect("Failed to spawn PTY I/O thread");
@@ -1771,8 +1775,6 @@ impl App {
return; // Window already exists return; // Window already exists
} }
log::info!("Creating window");
let mut window_attributes = Window::default_attributes() let mut window_attributes = Window::default_attributes()
.with_title("ZTerm") .with_title("ZTerm")
.with_inner_size(PhysicalSize::new(800, 600)); .with_inner_size(PhysicalSize::new(800, 600));
@@ -1802,13 +1804,10 @@ impl App {
self.window = Some(window); self.window = Some(window);
self.renderer = Some(renderer); self.renderer = Some(renderer);
self.should_create_window = false; self.should_create_window = false;
log::info!("Window created: {}x{} cells", cols, rows);
} }
/// Destroy the window but keep terminal state. /// Destroy the window but keep terminal state.
fn destroy_window(&mut self) { fn destroy_window(&mut self) {
log::info!("Destroying window (keeping terminal alive)");
self.renderer = None; self.renderer = None;
self.window = None; self.window = None;
} }
@@ -1977,8 +1976,8 @@ impl App {
.saturating_sub(pane.last_scrollback_len) .saturating_sub(pane.last_scrollback_len)
as isize; as isize;
if let Some(ref mut selection) = pane.selection { if let Some(ref mut selection) = pane.selection {
selection.start.row -= lines_added; selection.start.row = selection.start.row.saturating_sub(lines_added);
selection.end.row -= lines_added; selection.end.row = selection.end.row.saturating_sub(lines_added);
} }
pane.last_scrollback_len = scrollback_len; pane.last_scrollback_len = scrollback_len;
} }
@@ -1990,6 +1989,7 @@ impl App {
let active_tab_idx = self.active_tab; let active_tab_idx = self.active_tab;
let fade_duration_ms = self.config.inactive_pane_fade_ms; let fade_duration_ms = self.config.inactive_pane_fade_ms;
let inactive_dim = self.config.inactive_pane_dim; let inactive_dim = self.config.inactive_pane_dim;
let has_pending_redraw = self.renderer.as_ref().map(|r| r.has_pending_redraw()).unwrap_or(false);
if let Some(renderer) = &mut self.renderer { if let Some(renderer) = &mut self.renderer {
if let Some(tab) = self.tabs.get_mut(active_tab_idx) { if let Some(tab) = self.tabs.get_mut(active_tab_idx) {
@@ -2019,7 +2019,8 @@ impl App {
.unwrap_or(false) .unwrap_or(false)
}); });
if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress { let has_selection = tab.panes.values().any(|p| p.selection.is_some());
if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw {
let image_animation_in_progress = tab.panes.values().any(|p| { let image_animation_in_progress = tab.panes.values().any(|p| {
p.terminal.image_storage.has_animations() p.terminal.image_storage.has_animations()
}); });
@@ -2083,14 +2084,9 @@ impl App {
inactive_dim inactive_dim
}); });
// Convert selection to screen coords for this pane let selection = pane.selection.as_ref().and_then(|sel| {
let selection = if is_active { sel.to_screen_coords(scroll_offset, geom.rows)
pane.selection.as_ref().and_then(|sel| { });
sel.to_screen_coords(scroll_offset, geom.rows)
})
} else {
None
};
let render_info = PaneRenderInfo { let render_info = PaneRenderInfo {
pane_id: pane_id.0, pane_id: pane_id.0,
@@ -2155,9 +2151,9 @@ impl App {
StatuslineContent::Sections(Vec::new()) StatuslineContent::Sections(Vec::new())
} }
}) })
.unwrap_or_default(); .unwrap_or_default();
match renderer.render_panes( match renderer.render_panes(
&pane_render_data, &pane_render_data,
num_tabs, num_tabs,
active_tab_idx, active_tab_idx,
@@ -2165,13 +2161,17 @@ impl App {
self.config.edge_glow_intensity, self.config.edge_glow_intensity,
&statusline_content, &statusline_content,
) { ) {
Ok(_) => { Ok(_) => {
// Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean) // Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean)
for (pane_id, _) in &geometries { for (pane_id, _) in &geometries {
if let Some(pane) = tab.panes.get_mut(pane_id) { if let Some(pane) = tab.panes.get_mut(pane_id) {
pane.terminal.clear_dirty_lines(); pane.terminal.clear_dirty_lines();
} }
} }
// Clear pending redraw and needs_redraw
if let Some(renderer) = &mut self.renderer {
renderer.clear_pending_redraw();
}
// Only clear needs_redraw if no pane fade is still in progress // Only clear needs_redraw if no pane fade is still in progress
let any_fade_in_progress = geometries.iter().any(|(pane_id, _)| { let any_fade_in_progress = geometries.iter().any(|(pane_id, _)| {
tab.panes tab.panes
@@ -2182,12 +2182,12 @@ impl App {
}) })
.unwrap_or(false) .unwrap_or(false)
}); });
if !any_fade_in_progress { let any_selection = tab.panes.values().any(|p| p.selection.is_some());
if !any_fade_in_progress && !any_selection {
self.needs_redraw = false; self.needs_redraw = false;
} }
} }
Err(wgpu::SurfaceError::Lost) => { Err(wgpu::SurfaceError::Lost) => {
log::info!("RENDER LOST: resizing");
renderer.resize(renderer.width, renderer.height); renderer.resize(renderer.width, renderer.height);
} }
Err(wgpu::SurfaceError::OutOfMemory) => { Err(wgpu::SurfaceError::OutOfMemory) => {
@@ -2273,6 +2273,19 @@ impl App {
.unwrap_or(false) .unwrap_or(false)
} }
fn has_mouse_tracking_for_pane(&self, pane_id: PaneId) -> bool {
self.active_tab()
.and_then(|t| t.panes.get(&pane_id))
.map(|p| p.terminal.mouse_tracking != MouseTrackingMode::None)
.unwrap_or(false)
}
/// Find the pane geometry at a given pixel position in the active tab.
fn pane_at_pixel(&self, x: f64, y: f64) -> Option<PaneGeometry> {
self.active_tab()
.and_then(|t| t.split_root.find_pane_at_pixel(x, y))
}
fn get_mouse_modifiers(&self) -> u8 { fn get_mouse_modifiers(&self) -> u8 {
let mod_state = self.modifiers.state(); let mod_state = self.modifiers.state();
let mut mods = 0u8; let mut mods = 0u8;
@@ -2290,27 +2303,30 @@ impl App {
fn send_mouse_event( fn send_mouse_event(
&mut self, &mut self,
pane_id: PaneId,
button: u8, button: u8,
col: u16, col: u16,
row: u16, row: u16,
pressed: bool, pressed: bool,
is_motion: bool, is_motion: bool,
) { ) {
let seq = { let modifiers = self.get_mouse_modifiers();
let Some(pane) = self.active_pane() else { let Some(tab) = self.active_tab_mut() else {
return; return;
};
pane.terminal.encode_mouse(
button,
col,
row,
pressed,
is_motion,
self.get_mouse_modifiers(),
)
}; };
let Some(pane) = tab.panes.get_mut(&pane_id) else {
return;
};
let seq = pane.terminal.encode_mouse(
button,
col,
row,
pressed,
is_motion,
modifiers,
);
if !seq.is_empty() { if !seq.is_empty() {
self.write_to_pty(&seq); pane.pty.write(&seq);
} }
} }
@@ -2369,8 +2385,6 @@ impl App {
return false; return false;
}; };
log::info!("Executing action: {:?}", action);
self.execute_action(action); self.execute_action(action);
true true
} }
@@ -2384,13 +2398,19 @@ impl App {
self.paste_from_clipboard(); self.paste_from_clipboard();
} }
Action::NewTab => { Action::NewTab => {
if let Some(renderer) = &self.renderer { let (cols, rows) = if let Some(renderer) = &self.renderer {
let (cols, rows) = renderer.terminal_size(); renderer.terminal_size()
self.create_tab(cols, rows); } else {
// Resize the new tab to calculate pane geometries return;
self.resize_all_panes(); };
self.request_redraw(); self.create_tab(cols, rows);
// Resize the new tab to calculate pane geometries
self.resize_all_panes();
self.needs_redraw = true;
if let Some(renderer) = &mut self.renderer {
renderer.force_full_redraw();
} }
self.request_redraw();
} }
Action::ClosePane => { Action::ClosePane => {
self.close_active_pane(); self.close_active_pane();
@@ -2477,11 +2497,6 @@ impl App {
self.resize_all_panes(); self.resize_all_panes();
self.needs_redraw = true; self.needs_redraw = true;
self.request_redraw(); self.request_redraw();
log::info!(
"Split pane (horizontal={}), new pane {}",
horizontal,
pane_id.0
);
} }
} }
@@ -2855,23 +2870,21 @@ impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
#[cfg(feature = "render_timing")] #[cfg(feature = "render_timing")]
let start = std::time::Instant::now(); let start = std::time::Instant::now();
if self.window.is_none() { if self.window.is_none() {
self.create_window(event_loop); self.create_window(event_loop);
} }
#[cfg(feature = "render_timing")]
log::info!("App resumed (window creation): {:?}", start.elapsed());
} }
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event { match event {
UserEvent::ShowWindow => { UserEvent::ShowWindow => {
log::info!("Received signal to show window");
if self.window.is_none() { if self.window.is_none() {
self.create_window(event_loop); self.create_window(event_loop);
} }
} }
UserEvent::Tick => { UserEvent::Tick => {
log::info!("[MAIN] Tick received");
// Check for fatal render errors from previous frames // Check for fatal render errors from previous frames
if self.render_fatal_error { if self.render_fatal_error {
log::error!("Fatal render error occurred, exiting"); log::error!("Fatal render error occurred, exiting");
@@ -3011,16 +3024,6 @@ impl ApplicationHandler<UserEvent> for App {
let _ = proxy.send_event(UserEvent::Tick); let _ = proxy.send_event(UserEvent::Tick);
} }
} }
// Log every tick during benchmark for analysis (only with render_timing feature)
#[cfg(feature = "render_timing")]
{
let tick_time = tick_start.elapsed();
if tick_time.as_millis() > 5 {
log::info!("[TICK] render={:?} total={:?} has_more={} rendered={}",
render_time, tick_time, any_has_more, should_render);
}
}
} }
UserEvent::ConfigReloaded => { UserEvent::ConfigReloaded => {
self.reload_config(); self.reload_config();
@@ -3036,7 +3039,7 @@ impl ApplicationHandler<UserEvent> for App {
) { ) {
match event { match event {
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
log::info!("Window close requested - hiding window");
self.destroy_window(); self.destroy_window();
// Don't exit - keep running headless // Don't exit - keep running headless
} }
@@ -3046,7 +3049,7 @@ impl ApplicationHandler<UserEvent> for App {
} }
WindowEvent::ScaleFactorChanged { scale_factor, .. } => { WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
log::info!("Scale factor changed to {}", scale_factor);
let should_resize = if let Some(renderer) = &mut self.renderer { let should_resize = if let Some(renderer) = &mut self.renderer {
renderer.set_scale_factor(scale_factor) renderer.set_scale_factor(scale_factor)
} else { } else {
@@ -3085,23 +3088,45 @@ impl ApplicationHandler<UserEvent> for App {
}; };
if lines != 0 { if lines != 0 {
if self.has_mouse_tracking() { if let Some(tab) = self.active_tab() {
if let Some(renderer) = &self.renderer { if let Some(pane_geom) = tab.split_root.find_pane_at_pixel(
if let Some((col, row)) = renderer.pixel_to_cell( self.cursor_position.x,
self.cursor_position.x, self.cursor_position.y,
self.cursor_position.y, ) {
) { if let Some(renderer) = &self.renderer {
let button = if lines > 0 { 64 } else { 65 }; if let Some((col, row)) = renderer.pane_pixel_to_cell(
let count = lines.abs().min(3); self.cursor_position.x,
for _ in 0..count { self.cursor_position.y,
self.send_mouse_event( pane_geom.x,
button, col as u16, row as u16, true, pane_geom.y,
false, pane_geom.width,
); pane_geom.height,
pane_geom.cols,
pane_geom.rows,
) {
if self.has_mouse_tracking_for_pane(pane_geom.pane_id) {
let button = if lines > 0 { 64 } else { 65 };
let count = lines.abs().min(3);
let modifiers = self.get_mouse_modifiers();
for _ in 0..count {
if let Some(active_tab) = self.active_tab_mut() {
if let Some(pane) = active_tab.panes.get_mut(&pane_geom.pane_id) {
let seq = pane.terminal.encode_mouse(
button, col as u16, row as u16, true, false,
modifiers,
);
if !seq.is_empty() {
pane.pty.write(&seq);
}
}
}
}
}
} }
} }
} }
} else if let Some(tab) = self.active_tab_mut() { }
if let Some(tab) = self.active_tab_mut() {
// Positive lines = scroll wheel up = go into history (increase offset) // Positive lines = scroll wheel up = go into history (increase offset)
if let Some(pane) = tab.active_pane_mut() { if let Some(pane) = tab.active_pane_mut() {
pane.terminal.scroll(lines); pane.terminal.scroll(lines);
@@ -3114,48 +3139,146 @@ impl ApplicationHandler<UserEvent> for App {
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
self.cursor_position = position; self.cursor_position = position;
let is_selecting = // Find the pane under the mouse for pane-aware coordinate conversion
self.active_pane().map(|p| p.is_selecting).unwrap_or(false); let pane_geom = if let Some(tab) = self.active_tab() {
tab.split_root.find_pane_at_pixel(position.x, position.y)
} else {
None
};
if is_selecting && self.has_mouse_tracking() { // Switch focus if dragging on a different pane
// Send mouse drag/motion events to PTY for apps like Neovim if self.mouse_down_pos.is_some() {
if let Some(renderer) = &self.renderer { if let Some(geom) = &pane_geom {
if let Some((col, row)) = if let Some(tab) = self.active_tab_mut() {
renderer.pixel_to_cell(position.x, position.y) if tab.active_pane != geom.pane_id {
{ if tab.focus_pane_by_id(geom.pane_id) {
// Button 0 (left) with motion flag self.needs_redraw = true;
self.send_mouse_event( }
0, col as u16, row as u16, true, true, }
);
} }
} }
} else if is_selecting && !self.has_mouse_tracking() { }
// Check is_selecting on both the active pane and the pane under the mouse
let active_is_selecting =
self.active_pane().map(|p| p.is_selecting).unwrap_or(false);
let mouse_pane_is_selecting = pane_geom
.as_ref()
.and_then(|g| self.active_tab().and_then(|t| t.panes.get(&g.pane_id)))
.map(|p| p.is_selecting)
.unwrap_or(false);
let is_selecting = active_is_selecting || mouse_pane_is_selecting;
let mouse_tracking = pane_geom
.as_ref()
.map(|g| self.has_mouse_tracking_for_pane(g.pane_id))
.unwrap_or(false);
if is_selecting && mouse_tracking {
let modifiers = self.get_mouse_modifiers();
// Send mouse drag/motion events to PTY for apps like Neovim
if let Some(renderer) = &self.renderer {
if let Some(geom) = pane_geom {
if let Some((col, row)) = renderer.pane_pixel_to_cell(
position.x, position.y,
geom.x, geom.y,
geom.width, geom.height,
geom.cols, geom.rows,
) {
// Button 0 (left) with motion flag
if let Some(active_tab) = self.active_tab_mut() {
if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) {
let seq = pane.terminal.encode_mouse(
0, col as u16, row as u16, true, true,
modifiers,
);
if !seq.is_empty() {
pane.pty.write(&seq);
}
}
}
}
}
}
// Also update terminal-native selection for rendering
if let Some(renderer) = &self.renderer {
if let Some(geom) = pane_geom {
if let Some((col, screen_row)) = renderer.pane_pixel_to_cell(
position.x, position.y,
geom.x, geom.y,
geom.width, geom.height,
geom.cols, geom.rows,
) {
let scroll_offset = if let Some(tab) = self.active_tab() {
tab.panes.get(&geom.pane_id)
.map(|p| p.terminal.scroll_offset)
.unwrap_or(0)
} else { 0 };
let content_row =
screen_row as isize - scroll_offset as isize;
if let Some(active_tab) = self.active_tab_mut() {
if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) {
if let Some(ref mut selection) =
pane.selection
{
selection.end = CellPosition {
col,
row: content_row,
};
// Force GPU buffer upload so selection renders correctly
if let Some(renderer) = &mut self.renderer {
renderer.force_full_redraw();
}
}
}
}
}
}
}
} else if is_selecting && !mouse_tracking {
// Terminal-native selection // Terminal-native selection
if let Some(renderer) = &self.renderer { if let Some(renderer) = &self.renderer {
if let Some((col, screen_row)) = if let Some(geom) = pane_geom {
renderer.pixel_to_cell(position.x, position.y) if let Some((col, screen_row)) = renderer.pane_pixel_to_cell(
{ position.x, position.y,
let scroll_offset = self.get_scroll_offset(); geom.x, geom.y,
let content_row = geom.width, geom.height,
screen_row as isize - scroll_offset as isize; geom.cols, geom.rows,
) {
let scroll_offset = if let Some(tab) = self.active_tab() {
tab.panes.get(&geom.pane_id)
.map(|p| p.terminal.scroll_offset)
.unwrap_or(0)
} else { 0 };
let content_row =
screen_row as isize - scroll_offset as isize;
if let Some(tab) = self.active_tab_mut() { if let Some(active_tab) = self.active_tab_mut() {
if let Some(pane) = tab.active_pane_mut() { if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) {
if let Some(ref mut selection) = if let Some(ref mut selection) =
pane.selection pane.selection
{ {
selection.end = CellPosition { selection.end = CellPosition {
col, col,
row: content_row, row: content_row,
}; };
self.request_redraw(); if let Some(renderer) = &mut self.renderer {
renderer.force_full_redraw();
}
self.request_redraw();
}
} }
} }
} }
} }
} }
} else if let Some(down_pos) = self.mouse_down_pos { } else if let Some(down_pos) = self.mouse_down_pos {
if !self.has_mouse_tracking() { let mouse_pane_tracking = pane_geom
.as_ref()
.map(|g| self.has_mouse_tracking_for_pane(g.pane_id))
.unwrap_or(false);
if !mouse_pane_tracking {
let dx = position.x - down_pos.x; let dx = position.x - down_pos.x;
let dy = position.y - down_pos.y; let dy = position.y - down_pos.y;
let distance_sq = dx * dx + dy * dy; let distance_sq = dx * dx + dy * dy;
@@ -3170,40 +3293,56 @@ impl ApplicationHandler<UserEvent> for App {
if distance_sq > threshold * threshold { if distance_sq > threshold * threshold {
// Dragged far enough, start selection // Dragged far enough, start selection
if let Some(renderer) = &self.renderer { if let Some(renderer) = &self.renderer {
if let Some((start_col, start_screen_row)) = if let Some(geom) = pane_geom {
renderer if let Some((start_col, start_screen_row)) =
.pixel_to_cell(down_pos.x, down_pos.y) renderer.pane_pixel_to_cell(
{ down_pos.x, down_pos.y,
if let Some((end_col, end_screen_row)) = geom.x, geom.y,
renderer.pixel_to_cell( geom.width, geom.height,
position.x, position.y, geom.cols, geom.rows,
) )
{ {
let scroll_offset = if let Some((end_col, end_screen_row)) =
self.get_scroll_offset() as isize; renderer.pane_pixel_to_cell(
let start_pos = CellPosition { position.x, position.y,
col: start_col, geom.x, geom.y,
row: start_screen_row as isize geom.width, geom.height,
- scroll_offset, geom.cols, geom.rows,
}; )
let end_pos = CellPosition {
col: end_col,
row: end_screen_row as isize
- scroll_offset,
};
if let Some(tab) = self.active_tab_mut()
{ {
if let Some(pane) = let scroll_offset = if let Some(tab) = self.active_tab() {
tab.active_pane_mut() tab.panes.get(&geom.pane_id)
.map(|p| p.terminal.scroll_offset)
.unwrap_or(0) as isize
} else { 0 };
let start_pos = CellPosition {
col: start_col,
row: start_screen_row as isize
- scroll_offset,
};
let end_pos = CellPosition {
col: end_col,
row: end_screen_row as isize
- scroll_offset,
};
if let Some(active_tab) = self.active_tab_mut()
{ {
pane.selection = if let Some(pane) =
Some(Selection { active_tab.panes.get_mut(&geom.pane_id)
start: start_pos, {
end: end_pos, pane.selection =
}); Some(Selection {
pane.is_selecting = true; start: start_pos,
self.request_redraw(); end: end_pos,
});
pane.is_selecting = true;
// Force GPU buffer upload so selection renders correctly
if let Some(renderer) = &mut self.renderer {
renderer.force_full_redraw();
}
self.request_redraw();
}
} }
} }
} }
@@ -3222,34 +3361,74 @@ impl ApplicationHandler<UserEvent> for App {
_ => return, _ => return,
}; };
if self.has_mouse_tracking() { // Find the pane under the mouse for pane-aware coordinate conversion
let pane_geom = if let Some(tab) = self.active_tab() {
tab.split_root.find_pane_at_pixel(
self.cursor_position.x,
self.cursor_position.y,
)
} else {
None
};
// Switch focus if clicking on a different pane
if let Some(geom) = &pane_geom {
if let Some(tab) = self.active_tab_mut() {
if tab.active_pane != geom.pane_id {
if tab.focus_pane_by_id(geom.pane_id) {
self.needs_redraw = true;
}
}
}
}
let mouse_tracking = pane_geom
.as_ref()
.map(|g| self.has_mouse_tracking_for_pane(g.pane_id))
.unwrap_or(false);
if mouse_tracking {
if let Some(renderer) = &self.renderer { if let Some(renderer) = &self.renderer {
if let Some((col, row)) = renderer.pixel_to_cell( if let Some(geom) = pane_geom {
self.cursor_position.x, if let Some((col, row)) = renderer.pane_pixel_to_cell(
self.cursor_position.y, self.cursor_position.x,
) { self.cursor_position.y,
let pressed = state == ElementState::Pressed; geom.x, geom.y,
self.send_mouse_event( geom.width, geom.height,
button_code, geom.cols, geom.rows,
col as u16, ) {
row as u16, let pressed = state == ElementState::Pressed;
pressed, let modifiers = self.get_mouse_modifiers();
false, if let Some(active_tab) = self.active_tab_mut() {
); if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) {
if button == MouseButton::Left { let seq = pane.terminal.encode_mouse(
if let Some(tab) = self.active_tab_mut() { button_code,
if let Some(pane) = tab.active_pane_mut() { col as u16,
pane.is_selecting = pressed; row as u16,
pressed,
false,
modifiers,
);
if !seq.is_empty() {
pane.pty.write(&seq);
}
if button == MouseButton::Left {
pane.is_selecting = pressed;
}
} }
} }
} }
} }
} }
if let Some(tab) = self.active_tab_mut() { if mouse_tracking {
if let Some(pane) = tab.active_pane_mut() { if let Some(tab) = self.active_tab_mut() {
pane.selection = None; if let Some(pane) = tab.active_pane_mut() {
pane.selection = None;
}
} }
} }
if let Some(renderer) = &mut self.renderer {
renderer.force_full_redraw();
}
} else if button == MouseButton::Left { } else if button == MouseButton::Left {
match state { match state {
ElementState::Pressed => { ElementState::Pressed => {
@@ -3260,6 +3439,9 @@ impl ApplicationHandler<UserEvent> for App {
pane.is_selecting = false; pane.is_selecting = false;
} }
} }
if let Some(renderer) = &mut self.renderer {
renderer.force_full_redraw();
}
} }
ElementState::Released => { ElementState::Released => {
self.mouse_down_pos = None; self.mouse_down_pos = None;
@@ -3313,12 +3495,21 @@ impl ApplicationHandler<UserEvent> for App {
dl[0] != 0 || dl[1] != 0 || dl[2] != 0 || dl[3] != 0 dl[0] != 0 || dl[1] != 0 || dl[2] != 0 || dl[3] != 0
}) })
}); });
let need_render = any_dirty let need_render = any_dirty
|| self.needs_redraw || self.needs_redraw
|| !self.edge_glows.is_empty() || !self.edge_glows.is_empty()
|| self.tabs.iter().any(|tab| { || self.tabs.iter().any(|tab| {
tab.panes.values().any(|pane| pane.terminal.image_storage.has_animations()) tab.panes.values().any(|pane| pane.terminal.image_storage.has_animations())
}); })
|| self.tabs.iter().any(|tab| {
tab.panes.values().any(|pane| {
pane.selection.is_some() || pane.is_selecting
})
})
|| self.renderer.as_ref().map(|r| r.has_pending_redraw()).unwrap_or(false);
if need_render {
}
if !need_render { if !need_render {
return; return;
@@ -3335,33 +3526,11 @@ impl ApplicationHandler<UserEvent> for App {
// Log cumulative stats every second (only with render_timing feature) // Log cumulative stats every second (only with render_timing feature)
#[cfg(feature = "render_timing")] #[cfg(feature = "render_timing")]
if self.last_stats_log.elapsed() >= Duration::from_secs(1) { if self.last_stats_log.elapsed() >= Duration::from_secs(1) {
let parse_ms = self.total_parse_ns as f64 / 1_000_000.0;
let render_ms = self.total_render_ns as f64 / 1_000_000.0;
log::info!(
"STATS: parse={:.1}ms/{} render={:.1}ms/{} ratio={:.2}",
parse_ms,
self.parse_count,
render_ms,
self.render_count,
if parse_ms > 0.0 {
render_ms / parse_ms
} else {
0.0
}
);
self.total_parse_ns = 0; self.total_parse_ns = 0;
self.total_render_ns = 0; self.total_render_ns = 0;
self.parse_count = 0; self.parse_count = 0;
self.render_count = 0; self.render_count = 0;
self.last_stats_log = std::time::Instant::now(); self.last_stats_log = std::time::Instant::now();
}
#[cfg(feature = "render_timing")]
{
let frame_time = frame_start.elapsed();
if frame_time.as_millis() > 16 {
log::info!("Slow frame: {:?}", frame_time);
}
} }
} }
@@ -3369,10 +3538,9 @@ impl ApplicationHandler<UserEvent> for App {
} }
} }
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
// Check if all tabs have exited // Check if all tabs have exited
if self.tabs.is_empty() { if self.tabs.is_empty() {
log::info!("All tabs closed, exiting");
event_loop.exit(); event_loop.exit();
return; return;
} }
@@ -3382,7 +3550,6 @@ impl ApplicationHandler<UserEvent> for App {
let mut tabs_removed = false; let mut tabs_removed = false;
while i < self.tabs.len() { while i < self.tabs.len() {
if self.tabs[i].child_exited() { if self.tabs[i].child_exited() {
log::info!("Tab {} shell exited", i);
self.tabs.remove(i); self.tabs.remove(i);
tabs_removed = true; tabs_removed = true;
if self.active_tab >= self.tabs.len() && !self.tabs.is_empty() { if self.active_tab >= self.tabs.len() && !self.tabs.is_empty() {
@@ -3403,7 +3570,7 @@ impl ApplicationHandler<UserEvent> for App {
} }
if self.tabs.is_empty() { if self.tabs.is_empty() {
log::info!("All tabs closed, exiting");
event_loop.exit(); event_loop.exit();
return; return;
} }
@@ -3494,7 +3661,7 @@ fn setup_config_watcher(
return None; return None;
} }
log::info!("Config hot-reload enabled, watching {:?}", watch_path);
Some(watcher) Some(watcher)
} }
@@ -3504,11 +3671,11 @@ fn main() {
) )
.init(); .init();
log::info!("Starting ZTerm");
// Check for existing instance // Check for existing instance
if signal_existing_instance() { if signal_existing_instance() {
log::info!("Signaled existing instance, exiting");
return; return;
} }
+81 -112
View File
@@ -1552,6 +1552,53 @@ impl Renderer {
Some((col, row)) Some((col, row))
} }
/// Converts a pixel position to a terminal cell position relative to a specific pane.
/// `pane_x` and `pane_y` are the pane's pixel offset within the grid area.
/// `pane_width` and `pane_height` are the pane's dimensions in pixels.
/// `pane_cols` and `pane_rows` are the pane's dimensions in cells.
/// Returns None if the position is outside this pane's area.
pub fn pane_pixel_to_cell(
&self,
x: f64,
y: f64,
pane_x: f32,
pane_y: f32,
pane_width: f32,
pane_height: f32,
pane_cols: usize,
pane_rows: usize,
) -> Option<(usize, usize)> {
let terminal_y_offset = self.terminal_y_offset();
let grid_x_offset = self.grid_x_offset();
let grid_y_offset = self.grid_y_offset();
// Convert pane pixel offset to screen pixel offset
let pane_screen_x = grid_x_offset + pane_x;
let pane_screen_y = terminal_y_offset + grid_y_offset + pane_y;
// Check if position is within this pane's screen bounds
if (x as f32) < pane_screen_x
|| (x as f32) >= pane_screen_x + pane_width
|| (y as f32) < pane_screen_y
|| (y as f32) >= pane_screen_y + pane_height
{
return None;
}
// Calculate cell position relative to pane origin
let local_x = (x as f32) - pane_screen_x;
let local_y = (y as f32) - pane_screen_y;
let col = (local_x / self.cell_metrics.cell_width as f32).floor() as usize;
let row = (local_y / self.cell_metrics.cell_height as f32).floor() as usize;
if col >= pane_cols || row >= pane_rows {
return None;
}
Some((col, row))
}
/// Updates the scale factor and recalculates font/cell dimensions. /// Updates the scale factor and recalculates font/cell dimensions.
/// Returns true if the cell dimensions changed (terminal needs resize). /// Returns true if the cell dimensions changed (terminal needs resize).
pub fn set_scale_factor(&mut self, new_scale: f64) -> bool { pub fn set_scale_factor(&mut self, new_scale: f64) -> bool {
@@ -1589,11 +1636,6 @@ impl Renderer {
// Update the font units to pixels scale factor // Update the font units to pixels scale factor
self.font_units_to_px = self.font_size / self.primary_font.height_unscaled(); self.font_units_to_px = self.font_size / self.primary_font.height_unscaled();
log::info!(
"Scale factor changed to {}: font {}px -> {}px, cell: {}x{}, baseline: {}",
new_scale, self.base_font_size, self.font_size, self.cell_metrics.cell_width, self.cell_metrics.cell_height, self.cell_metrics.baseline
);
// Reset atlas and all sprite/glyph caches (includes cursor sprite creation) // Reset atlas and all sprite/glyph caches (includes cursor sprite creation)
self.reset_atlas(); self.reset_atlas();
@@ -1648,11 +1690,6 @@ impl Renderer {
// Update the font units to pixels scale factor // Update the font units to pixels scale factor
self.font_units_to_px = self.font_size / self.primary_font.height_unscaled(); self.font_units_to_px = self.font_size / self.primary_font.height_unscaled();
log::info!(
"Font size changed to {}px -> {}px, cell: {}x{}, baseline: {}",
size, self.font_size, self.cell_metrics.cell_width, self.cell_metrics.cell_height, self.cell_metrics.baseline
);
// Reset atlas and all sprite/glyph caches (includes cursor sprite creation) // Reset atlas and all sprite/glyph caches (includes cursor sprite creation)
self.reset_atlas(); self.reset_atlas();
@@ -1666,7 +1703,7 @@ impl Renderer {
/// NOTE: This should ONLY be called for font/scale changes, NOT when atlas is full /// NOTE: This should ONLY be called for font/scale changes, NOT when atlas is full
/// (for that case, we add a new layer via add_atlas_layer()). /// (for that case, we add a new layer via add_atlas_layer()).
fn reset_atlas(&mut self) { fn reset_atlas(&mut self) {
log::info!("Resetting glyph atlas (font/scale changed)");
// Clear all glyph caches - they need to be re-rasterized at new size // Clear all glyph caches - they need to be re-rasterized at new size
self.char_cache.clear(); self.char_cache.clear();
@@ -2146,6 +2183,16 @@ impl Renderer {
self.cells_dirty = true; self.cells_dirty = true;
} }
/// Check if a full redraw is pending.
pub fn has_pending_redraw(&self) -> bool {
self.cells_dirty
}
/// Called after a render to clear the pending redraw flag.
pub fn clear_pending_redraw(&mut self) {
self.cells_dirty = false;
}
/// Update GPU cell buffer from terminal content. /// Update GPU cell buffer from terminal content.
/// Like Kitty, this only processes dirty lines to minimize work. /// Like Kitty, this only processes dirty lines to minimize work.
/// ///
@@ -2163,14 +2210,17 @@ impl Renderer {
self.cells_dirty = true; self.cells_dirty = true;
} }
// Check if this terminal has any dirty lines
let has_dirty = terminal.has_any_dirty_line();
// First pass: ensure all characters have sprites // First pass: ensure all characters have sprites
// This needs mutable access to self for sprite creation // This needs mutable access to self for sprite creation
// Like Kitty's render_line(), detect PUA+space patterns for multi-cell rendering // Like Kitty's render_line(), detect PUA+space patterns for multi-cell rendering
// OPTIMIZATION: Only process dirty lines or when full rebuild is needed // OPTIMIZATION: Only process dirty lines or when full rebuild is needed
// OPTIMIZATION: Use get_visible_row() to avoid Vec allocation // OPTIMIZATION: Use get_visible_row() to avoid Vec allocation
for row_idx in 0..rows { for row_idx in 0..rows {
// Skip clean lines (unless size changed, which sets cells_dirty) // Skip clean lines (unless size changed or terminal has dirty lines)
if !self.cells_dirty && !terminal.is_line_dirty(row_idx) { if !self.cells_dirty && !has_dirty && !terminal.is_line_dirty(row_idx) {
continue; continue;
} }
@@ -2344,61 +2394,21 @@ impl Renderer {
} }
// Second pass: convert cells to GPU format // Second pass: convert cells to GPU format
// OPTIMIZATION: Use get_visible_row() to avoid Vec allocation // Always update self.gpu_cells from the current terminal to avoid
// stale data from a previous pane being written to the wrong GPU buffer.
let mut any_updated = false; let mut any_updated = false;
// DEBUG: Log grid dimensions and buffer state for row_idx in 0..rows {
static DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); if let Some(row) = terminal.get_visible_row(row_idx) {
let frame_num = DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let start = row_idx * cols;
if frame_num % 60 == 0 { // Log every 60 frames (~1 second at 60fps) let end = start + cols;
log::info!("DEBUG update_gpu_cells: cols={} rows={} total={} gpu_cells.len={} cells_dirty={}",
cols, rows, total_cells, self.gpu_cells.len(), self.cells_dirty); if end > self.gpu_cells.len() {
} continue;
// If we did a full reset or size changed, update all lines
if self.cells_dirty {
static ROW_DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let row_frame = ROW_DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if row_frame % 60 == 0 {
let first_col: String = (0..rows).filter_map(|r| {
terminal.get_visible_row(r).and_then(|row| {
row.first().map(|cell| {
let c = cell.character;
if c == '\0' { ' ' } else { c }
})
})
}).collect();
log::info!("DEBUG col0: \"{}\"", first_col);
}
for row_idx in 0..rows {
if let Some(row) = terminal.get_visible_row(row_idx) {
let start = row_idx * cols;
let end = start + cols;
if end > self.gpu_cells.len() {
log::error!("DEBUG BUG: row_idx={} start={} end={} but gpu_cells.len={}",
row_idx, start, end, self.gpu_cells.len());
continue;
}
Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map);
}
}
self.cells_dirty = false;
any_updated = true;
} else {
// Only update dirty lines - use is_line_dirty() which handles all 256 lines
for row_idx in 0..rows {
if terminal.is_line_dirty(row_idx) {
if let Some(row) = terminal.get_visible_row(row_idx) {
let start = row_idx * cols;
let end = start + cols;
Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map);
any_updated = true;
}
} }
Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map);
any_updated = true;
} }
} }
@@ -3681,7 +3691,7 @@ impl Renderer {
return; return;
} }
log::info!("Adding atlas layer {} (was on layer {})", new_layer, self.atlas_current_layer);
// Create real texture for the new layer (replacing the dummy) // Create real texture for the new layer (replacing the dummy)
self.ensure_atlas_layer_capacity(new_layer); self.ensure_atlas_layer_capacity(new_layer);
@@ -3708,7 +3718,7 @@ impl Renderer {
return; return;
} }
log::info!("Adding atlas layer {} (replacing dummy texture)", target_layer);
// Create new real texture (8192x8192) // Create new real texture (8192x8192)
let texture = self.device.create_texture(&wgpu::TextureDescriptor { let texture = self.device.create_texture(&wgpu::TextureDescriptor {
@@ -4636,7 +4646,7 @@ impl Renderer {
{ {
let update_time = t0.elapsed(); let update_time = t0.elapsed();
if update_time.as_micros() > 500 { if update_time.as_micros() > 500 {
log::info!("update_gpu_cells took {:?}", update_time);
} }
} }
@@ -4679,36 +4689,7 @@ impl Renderer {
selection_end_col: sel_end_col, selection_end_col: sel_end_col,
selection_end_row: sel_end_row, selection_end_row: sel_end_row,
}; };
// DEBUG: Log grid params every 60 frames
static PANE_DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let pane_frame = PANE_DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if pane_frame % 60 == 0 {
log::info!("DEBUG pane {}: grid_params cols={} rows={} gpu_cells.len={} expected={}",
info.pane_id, grid_params.cols, grid_params.rows,
self.gpu_cells.len(), (grid_params.cols * grid_params.rows) as usize);
// Sample a few cells to see if sprite indices look reasonable
if !self.gpu_cells.is_empty() {
let sample_indices = [0, 1, 2, cols as usize, cols as usize + 1];
for &idx in &sample_indices {
if idx < self.gpu_cells.len() {
let cell = &self.gpu_cells[idx];
let sprite_idx = cell.sprite_idx & !0x80000000;
log::info!("DEBUG cell[{}]: sprite_idx={} fg={:#x} bg={:#x}",
idx, sprite_idx, cell.fg, cell.bg);
if sprite_idx > 0 && (sprite_idx as usize) < self.sprite_info.len() {
let sprite = &self.sprite_info[sprite_idx as usize];
log::info!("DEBUG sprite[{}]: uv=({:.3},{:.3},{:.3},{:.3}) layer={} size=({:.1},{:.1})",
sprite_idx, sprite.uv[0], sprite.uv[1], sprite.uv[2], sprite.uv[3],
sprite.layer, sprite.size[0], sprite.size[1]);
}
}
}
}
}
// Upload this pane's cell data to its own buffer (like Kitty's send_cell_data_to_gpu) // Upload this pane's cell data to its own buffer (like Kitty's send_cell_data_to_gpu)
// This happens BEFORE the render pass, so each pane has its own data // This happens BEFORE the render pass, so each pane has its own data
if let Some(pane_res) = self.pane_resources.get(&info.pane_id) { if let Some(pane_res) = self.pane_resources.get(&info.pane_id) {
@@ -4770,7 +4751,7 @@ impl Renderer {
{ {
let pane_loop_time = pane_loop_start.elapsed(); let pane_loop_time = pane_loop_start.elapsed();
if pane_loop_time.as_micros() > 500 { if pane_loop_time.as_micros() > 500 {
log::info!("pane_loop took {:?}", pane_loop_time);
} }
} }
@@ -5258,20 +5239,8 @@ impl Renderer {
#[cfg(feature = "render_timing")] #[cfg(feature = "render_timing")]
let after_submit = frame_start.elapsed(); let after_submit = frame_start.elapsed();
output.present(); output.present();
// Log timing if frame took more than 1ms (only with render_timing feature)
#[cfg(feature = "render_timing")]
{
let after_present = frame_start.elapsed();
if after_present.as_micros() > 1000 {
log::info!("render_panes: before_submit={:?} submit={:?} present={:?} total={:?}",
before_submit,
after_submit - before_submit,
after_present - after_submit,
after_present);
}
}
self.clear_pending_redraw();
Ok(()) Ok(())
} }
+8 -103
View File
@@ -298,87 +298,6 @@ struct AlternateScreen {
scroll_bottom: usize, scroll_bottom: usize,
} }
/// Timing stats for performance debugging.
/// Only populated when the `render_timing` feature is enabled.
#[derive(Debug, Default)]
pub struct ProcessingStats {
#[cfg(feature = "render_timing")]
/// Total time spent in scroll_up operations (nanoseconds).
pub scroll_up_ns: u64,
#[cfg(feature = "render_timing")]
/// Number of scroll_up calls.
pub scroll_up_count: u32,
#[cfg(feature = "render_timing")]
/// Total time spent in scrollback operations (nanoseconds).
pub scrollback_ns: u64,
#[cfg(feature = "render_timing")]
/// Time in VecDeque pop_front.
pub pop_front_ns: u64,
#[cfg(feature = "render_timing")]
/// Time in VecDeque push_back.
pub push_back_ns: u64,
#[cfg(feature = "render_timing")]
/// Time in mem::swap.
pub swap_ns: u64,
#[cfg(feature = "render_timing")]
/// Total time spent in line clearing (nanoseconds).
pub clear_line_ns: u64,
#[cfg(feature = "render_timing")]
/// Total time spent in text handler (nanoseconds).
pub text_handler_ns: u64,
#[cfg(feature = "render_timing")]
/// Total time spent in CSI handler (nanoseconds).
pub csi_handler_ns: u64,
#[cfg(feature = "render_timing")]
/// Number of CSI sequences processed.
pub csi_count: u32,
#[cfg(feature = "render_timing")]
/// Number of characters processed.
pub chars_processed: u32,
#[cfg(feature = "render_timing")]
/// Total time spent in VT parser (consume_input) - nanoseconds.
pub vt_parser_ns: u64,
#[cfg(feature = "render_timing")]
/// Number of consume_input calls.
pub consume_input_count: u32,
}
impl ProcessingStats {
#[cfg(feature = "render_timing")]
pub fn reset(&mut self) {
*self = Self::default();
}
#[cfg(not(feature = "render_timing"))]
pub fn reset(&mut self) {}
#[cfg(feature = "render_timing")]
pub fn log_if_slow(&self, threshold_ms: u64) {
let total_ms =
(self.scroll_up_ns + self.text_handler_ns + self.csi_handler_ns)
/ 1_000_000;
if total_ms >= threshold_ms {
let vt_only_ns = self
.vt_parser_ns
.saturating_sub(self.text_handler_ns + self.csi_handler_ns);
log::info!(
"[PARSE_DETAIL] text={:.2}ms ({}chars) csi={:.2}ms ({}x) vt_only={:.2}ms ({}calls) scroll={:.2}ms ({}x)",
self.text_handler_ns as f64 / 1_000_000.0,
self.chars_processed,
self.csi_handler_ns as f64 / 1_000_000.0,
self.csi_count,
vt_only_ns as f64 / 1_000_000.0,
self.consume_input_count,
self.scroll_up_ns as f64 / 1_000_000.0,
self.scroll_up_count,
);
}
}
#[cfg(not(feature = "render_timing"))]
pub fn log_if_slow(&self, _threshold_ms: u64) {}
}
/// Kitty-style ring buffer for scrollback history. /// Kitty-style ring buffer for scrollback history.
/// ///
/// Pre-allocates all lines upfront to avoid allocation during scrolling. /// Pre-allocates all lines upfront to avoid allocation during scrolling.
@@ -569,7 +488,7 @@ pub struct Terminal {
/// Synchronized output mode (for reducing flicker). /// Synchronized output mode (for reducing flicker).
synchronized_output: bool, synchronized_output: bool,
/// Performance timing stats (for debugging). /// Performance timing stats (for debugging).
pub stats: ProcessingStats,
/// Command queue for terminal-to-application communication. /// Command queue for terminal-to-application communication.
/// Commands are added by OSC handlers and consumed by the application. /// Commands are added by OSC handlers and consumed by the application.
command_queue: Vec<TerminalCommand>, command_queue: Vec<TerminalCommand>,
@@ -587,12 +506,6 @@ impl Terminal {
/// Creates a new terminal with the given dimensions and scrollback limit. /// Creates a new terminal with the given dimensions and scrollback limit.
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self { pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
log::info!(
"Terminal::new: cols={}, rows={}, scroll_bottom={}",
cols,
rows,
rows.saturating_sub(1)
);
let grid = vec![vec![Cell::default(); cols]; rows]; let grid = vec![vec![Cell::default(); cols]; rows];
let line_map: Vec<usize> = (0..rows).collect(); let line_map: Vec<usize> = (0..rows).collect();
@@ -632,7 +545,7 @@ impl Terminal {
bracketed_paste: false, bracketed_paste: false,
focus_reporting: false, focus_reporting: false,
synchronized_output: false, synchronized_output: false,
stats: ProcessingStats::default(),
command_queue: Vec::new(), command_queue: Vec::new(),
image_storage: ImageStorage::new(), image_storage: ImageStorage::new(),
cell_width: 10.0, // Default, will be set by renderer cell_width: 10.0, // Default, will be set by renderer
@@ -668,6 +581,12 @@ impl Terminal {
} }
} }
/// Check if any line is dirty.
#[inline]
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
}
/// Clear all dirty line flags. /// Clear all dirty line flags.
#[inline] #[inline]
pub fn clear_dirty_lines(&mut self) { pub fn clear_dirty_lines(&mut self) {
@@ -789,14 +708,6 @@ impl Terminal {
return; return;
} }
log::info!(
"Terminal::resize: {}x{} -> {}x{}",
self.cols,
self.rows,
cols,
rows
);
let old_cols = self.cols; let old_cols = self.cols;
let old_rows = self.rows; let old_rows = self.rows;
@@ -1692,12 +1603,6 @@ impl Handler for Terminal {
let statusline = let statusline =
content.filter(|s| !s.is_empty()); content.filter(|s| !s.is_empty());
log::info!(
"OSC 51: Set statusline: {:?}",
statusline
.as_ref()
.map(|s| format!("{} bytes", s.len()))
);
self.command_queue.push( self.command_queue.push(
TerminalCommand::SetStatusline(statusline), TerminalCommand::SetStatusline(statusline),
); );