diff --git a/README.md b/README.md deleted file mode 100644 index 056361a..0000000 --- a/README.md +++ /dev/null @@ -1,350 +0,0 @@ -# simplicitty - -A simple terminal emulator - -## 1. Implement Incremental Rendering - -Currently, your renderer appears to redraw the entire terminal content on each -frame. For a terminal emulator, this is inefficient since only small portions -typically change at once. - -rust // Add to TerminalText struct dirty_regions: Vec, // Store -regions that need updating last_frame_content: String, // Store previous frame's -content - -Only redraw areas that have changed since the last frame by tracking dirty -regions. - -## 2. Implement Text Caching - -The current implementation reshapes all text on every frame. Instead: - -rust // Add to TerminalText cached_lines: HashMap, - -Cache shaped text lines and only reshape when content changes or window resizes. - -## 3. Use Double Buffering for Text - -Maintain two buffers - one for the current frame and one for the next: - -rust // In TerminalText front_buffer: Buffer, back_buffer: Buffer, - -Prepare the back buffer while rendering the front buffer, then swap them. - -## 4. Optimize Atlas Trimming - -The trim() call after every frame can be expensive. Consider: - -rust // In TerminalText frames_since_trim: u32, - -pub fn trim(&mut self) { self.frames_since_trim += 1; if -self.frames_since_trim > 30 { // Only trim every 30 frames self.atlas.trim(); -self.frames_since_trim = 0; } } - -## 5. Implement View Culling - -Only render text that's actually visible: - -rust // In prepare() method let visible_area = TextBounds { left: 0, top: -scroll_position, right: config.width as i32, bottom: scroll_position + -config.height as i32, }; - -## 6. Use Asynchronous Text Preparation - -Move text shaping to a background thread: - -rust // In Application struct text_preparation_pool: ThreadPool, - -// Then queue text preparation tasks self.text_preparation_pool.spawn(move || { -// Shape text here // Signal completion via channel }); - -## 7. Optimize GPU Memory Usage - -The current implementation might be creating new GPU resources frequently. -Implement resource pooling: - -rust // In TerminalText texture_pool: Vec, // Reuse textures -instead of creating new ones - -## 8. Implement Partial Updates for the Text Atlas - -Instead of rebuilding the entire text atlas, update only changed glyphs: - -rust // Add method to TextAtlas pub fn update_region(&mut self, device: &Device, -queue: &Queue, region: Rectangle) { // Update only the specified region } - -## 9. Use Compute Shaders for Text Processing - -For complex text operations, consider using compute shaders: - -rust // Create a compute pipeline for text processing let compute_pipeline = -device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { // Configure -for text processing }); - -## 10. Implement Frame Skipping - -Skip rendering frames when nothing has changed: - -rust pub fn render(&mut self) -> Result<(), SurfaceError> { if !self.has_changes -{ return Ok(()); } // Render as normal } - -## 11. Profile and Optimize Hot Paths - -Use a profiler to identify bottlenecks in your rendering pipeline. Common issues -might be: • Excessive GPU state changes • Inefficient text shaping • Unnecessary -buffer updates - -## 12. Implement Proper Vsync Control - -Your current implementation uses PresentMode::Fifo (VSync), which is good for -power efficiency but might not be optimal for all cases: - -rust // Allow configurable present mode based on user preference present_mode: -if vsync_enabled { wgpu::PresentMode::Fifo } else { wgpu::PresentMode::Immediate -}, - -Based on your project structure and the README.md optimization suggestions, -here's how you can implement incremental text buffer updates: - -## 1. Track Changes with Dirty Regions - -First, modify your TerminalText struct to track which parts of the text have -changed: - -rust pub struct TerminalText { font_system: FontSystem, cache: SwashCache, -viewport: Viewport, atlas: TextAtlas, buffer: Buffer, renderer: TextRenderer, - - // Add these fields to track changes - last_content: String, - dirty_regions: Vec, - cursor_position: usize, - -} - -## 2. Implement Incremental Text Updates - -Instead of replacing the entire text on each update, implement methods to -insert, delete, or modify text at specific positions: - -```rust -impl TerminalText { // Add a character at the cursor position pub fn -insert_char(&mut self, c: char) { let current_text = -self.buffer.text().to_string(); let pos = -self.cursor_position.min(current_text.len()); - - // Create new text with the character inserted - let mut new_text = current_text.clone(); - new_text.insert(pos, c); - - // Calculate the affected region - let affected_line = self.calculate_line_for_position(pos); - let line_bounds = self.get_line_bounds(affected_line); - self.dirty_regions.push(line_bounds); - - // Update the buffer with the new text - self.buffer.set_text( - &mut self.font_system, - &new_text, - &Attrs::new().family(Family::Monospace), - Shaping::Advanced, - ); - - // Update cursor position - self.cursor_position = pos + 1; - self.last_content = new_text; - } - - // Delete a character before the cursor - pub fn delete_char(&mut self) { - let current_text = self.buffer.text().to_string(); - if self.cursor_position > 0 && !current_text.is_empty() { - let pos = self.cursor_position.min(current_text.len()); - - // Create new text with the character deleted - let mut new_text = current_text.clone(); - new_text.remove(pos - 1); - - // Calculate the affected region - let affected_line = self.calculate_line_for_position(pos - 1); - let line_bounds = self.get_line_bounds(affected_line); - self.dirty_regions.push(line_bounds); - - // Update the buffer with the new text - self.buffer.set_text( - &mut self.font_system, - &new_text, - &Attrs::new().family(Family::Monospace), - Shaping::Advanced, - ); - - // Update cursor position - self.cursor_position = pos - 1; - self.last_content = new_text; - } - } - - // Helper methods to calculate affected regions - fn calculate_line_for_position(&self, pos: usize) -> usize { - // Count newlines up to the position - self.buffer.text()[..pos].matches('\n').count() - } - - fn get_line_bounds(&self, line: usize) -> TextBounds { - // Calculate the bounds for a specific line - // This is a simplified version - you'll need to implement this based on your text layout - TextBounds { - left: 0, - top: (line * self.get_line_height()) as i32, - right: self.viewport.width() as i32, - bottom: ((line + 1) * self.get_line_height()) as i32, - } - } - - fn get_line_height(&self) -> usize { - // Get the line height based on your font metrics - self.buffer.metrics().line_height.ceil() as usize - } - -} -``` - -## 3. Optimize the Rendering Process - -Modify your prepare and render methods to only update the dirty regions: - -```rust -pub fn prepare(&mut self, device: &Device, queue: &Queue, config: -&SurfaceConfiguration) { // Update viewport with new dimensions -self.viewport.update( queue, Resolution { width: config.width, height: -config.height, }, ); - - // Make sure text is properly wrapped for the current width - self.resize(config.width, config.height); - - // Only reshape if there are dirty regions - if !self.dirty_regions.is_empty() { - // Prepare only the dirty regions - for region in &self.dirty_regions { - self.renderer - .prepare( - device, - queue, - &mut self.font_system, - &mut self.atlas, - &self.viewport, - [TextArea { - buffer: &self.buffer, - left: 0.0, - top: 0.0, - scale: 1.0, - bounds: *region, - default_color: Color::rgb(255, 255, 255), - custom_glyphs: &[], - }], - &mut self.cache, - ) - .unwrap(); - } - - // Clear dirty regions after preparing - self.dirty_regions.clear(); - } - -} -``` - -## 4. Implement a Double Buffer Approach - -To further optimize, implement a double buffer approach: - -```rust -pub struct TerminalText { // ... existing fields front_buffer: Buffer, -back_buffer: Buffer, buffer_swap_needed: bool, } - -impl TerminalText { // ... existing methods - - pub fn swap_buffers(&mut self) { - if self.buffer_swap_needed { - std::mem::swap(&mut self.front_buffer, &mut self.back_buffer); - self.buffer_swap_needed = false; - } - } - - pub fn prepare_back_buffer(&mut self, device: &Device, queue: &Queue) { - // Prepare the back buffer while the front buffer is being rendered - if !self.dirty_regions.is_empty() { - // Apply changes to the back buffer - // ... - - self.buffer_swap_needed = true; - } - } - -} -``` - -## 5. Implement Cursor Rendering - -Add cursor rendering to provide visual feedback: - -rust pub fn render_cursor(&mut self, render_pass: &mut RenderPass) { // -Calculate cursor position in screen coordinates let (x, y) = -self.get_cursor_screen_position(); - - // Render cursor using a simple rectangle - // You'll need to implement this based on your rendering system - -} - -fn get_cursor_screen_position(&self) -> (f32, f32) { // Calculate screen -position based on cursor_position // This will depend on your text layout system -// ... } - -## 6. Handle Special Keys - -In your keyboard event handler, handle special keys like backspace, enter, and -arrow keys: - -rust fn handle*key_event(&mut self, key_event: KeyEvent) { match key_event.key { -Key::Character(c) => { if key_event.state == ElementState::Pressed { -self.text.insert_char(c.chars().next().unwrap_or(' ')); -self.window.request_redraw(); } }, Key::Named(NamedKey::Backspace) => { if -key_event.state == ElementState::Pressed { self.text.delete_char(); -self.window.request_redraw(); } }, Key::Named(NamedKey::Enter) => { if -key_event.state == ElementState::Pressed { self.text.insert_char('\n'); -self.window.request_redraw(); } }, // Handle arrow keys for cursor movement -Key::Named(NamedKey::ArrowLeft) => { if key_event.state == ElementState::Pressed -{ self.text.move_cursor_left(); self.window.request_redraw(); } }, -Key::Named(NamedKey::ArrowRight) => { if key_event.state == -ElementState::Pressed { self.text.move_cursor_right(); -self.window.request_redraw(); } }, * => {} } } - -## 7. Implement Frame Skipping - -Skip rendering frames when nothing has changed: - -rust pub fn render(&mut self) -> Result<(), SurfaceError> { // Skip rendering if -nothing has changed if self.dirty_regions.is_empty() && !self.buffer_swap_needed -{ return Ok(()); } - - // Swap buffers if needed - self.text.swap_buffers(); - - // Render as normal - // ... - - Ok(()) - -} - -These changes will significantly improve the efficiency of your text rendering -by: - -1. Only updating parts of the text that have changed -2. Using double buffering to prepare updates while rendering -3. Skipping frames when nothing has changed -4. Optimizing the text atlas updates - -This approach aligns with the optimization suggestions in your README.md and -should provide a much more efficient text rendering system for your terminal -emulator. diff --git a/src/app/mod.rs b/src/app/mod.rs index eefc9aa..87a9e7b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use tokio::runtime::Runtime; use winit::dpi::LogicalSize; use winit::event::WindowEvent; -use winit::keyboard::{Key, NamedKey}; use winit::window::Window; mod rendering; @@ -60,23 +59,7 @@ impl winit::application::ApplicationHandler for Application { is_synthetic: _, } => { if event.state.is_pressed() { - match event.logical_key { - Key::Named(k) => match k { - NamedKey::Enter => { - state.handle_enter(); - } - _ => todo!(), - }, - Key::Character(c) => { - state.add_text(&event.text.unwrap()); - } - Key::Dead(_) => { - todo!() - } - Key::Unidentified(_) => { - todo!() - } - } + state.handle_keyboard_input(&event); } } WindowEvent::CloseRequested => event_loop.exit(), diff --git a/src/app/rendering/mod.rs b/src/app/rendering/mod.rs index 93b465f..a65f9e9 100644 --- a/src/app/rendering/mod.rs +++ b/src/app/rendering/mod.rs @@ -6,6 +6,8 @@ use wgpu::{ TextureViewDescriptor, }; use winit::dpi::PhysicalSize; +use winit::event::KeyEvent; +use winit::keyboard::{Key, NamedKey}; use winit::window::Window; mod terminal_text; @@ -30,6 +32,7 @@ impl State { self.config.width = new_size.width; self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); + self.text.resize(new_size.width, new_size.height); self.window.request_redraw(); } } @@ -187,15 +190,26 @@ impl State { } } - pub fn add_text(&mut self, text: &str) { - for c in text.chars() { - self.text.insert_char(c); + pub fn handle_keyboard_input(&mut self, event: &KeyEvent) { + match &event.logical_key { + Key::Named(k) => match k { + NamedKey::Enter => { + self.text.handle_enter(); + } + _ => todo!(), + }, + Key::Character(s) => { + for c in s.chars() { + self.text.insert_char(c); + } + } + Key::Dead(_) => { + todo!() + } + Key::Unidentified(_) => { + todo!() + } } self.window.request_redraw(); } - - pub fn handle_enter(&mut self) { - self.text.handle_enter(); - self.window.request_redraw(); - } } diff --git a/src/app/rendering/terminal_text/mod.rs b/src/app/rendering/terminal_text/mod.rs index 04a67b9..d11656d 100644 --- a/src/app/rendering/terminal_text/mod.rs +++ b/src/app/rendering/terminal_text/mod.rs @@ -1,6 +1,6 @@ use glyphon::{ - Attrs, Buffer, BufferLine, Cache, Color, Cursor, Family, FontSystem, Metrics, Resolution, - Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap, + Buffer, BufferLine, Cache, Color, Cursor, FontSystem, Metrics, Resolution, Shaping, SwashCache, + TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap, }; use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat}; @@ -16,10 +16,11 @@ pub struct TerminalText { cursor: Cursor, } -use log::info; +use log::{debug, trace}; impl TerminalText { pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText { + debug!("Creating a new TerminalText object"); let mut font_system = FontSystem::new(); let swash_cache = SwashCache::new(); @@ -32,6 +33,7 @@ impl TerminalText { let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25)); buffer.set_wrap(&mut font_system, Wrap::Glyph); + trace!("Loading system fonts"); // Add a default font font_system.db_mut().load_system_fonts(); @@ -51,27 +53,42 @@ impl TerminalText { } } - pub fn set_text(&mut self, text: &str) { - // Clear the buffer and set new text - self.buffer.set_text( - &mut self.font_system, - text, - &Attrs::new().family(Family::Monospace), - Shaping::Advanced, - ); - self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph); - } - pub fn resize(&mut self, width: u32, height: u32) { - // Update the buffer's wrapping based on the new width - self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph); + trace!("Resizing window - Width: {width} Height: {height}"); self.buffer.set_size( &mut self.font_system, Some(width as f32), Some(height as f32), ); + // Update the buffer's wrapping based on the new width + self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph); // Reshape the text with the new dimensions self.buffer.shape_until_scroll(&mut self.font_system, false); + self.ensure_visible_text_rendered(); + } + + fn merge_dirty_regions(&self) -> TextBounds { + TextBounds { + left: self + .dirty_regions + .iter() + .min_by_key(|b| b.left) + .unwrap() + .left, + top: self.dirty_regions.iter().min_by_key(|b| b.top).unwrap().top, + right: self + .dirty_regions + .iter() + .max_by_key(|b| b.right) + .unwrap() + .right, + bottom: self + .dirty_regions + .iter() + .max_by_key(|b| b.bottom) + .unwrap() + .bottom, + } } pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) { @@ -84,33 +101,9 @@ impl TerminalText { }, ); - // Make sure text is properly wrapped for the current width - self.resize(config.width, config.height); - if !self.dirty_regions.is_empty() { - for region in &self.dirty_regions { - self.renderer - .prepare( - device, - queue, - &mut self.font_system, - &mut self.atlas, - &self.viewport, - [TextArea { - buffer: &self.buffer, - left: 0.0, - top: 0.0, - scale: 1.0, - bounds: *region, - default_color: Color::rgb(255, 255, 255), - custom_glyphs: &[], - }], - &mut self.cache, - ) - .unwrap(); - } - } else { - // If no dirty regions, prepare the entire buffer (first render or after resize) + let region = self.merge_dirty_regions(); + trace!("Preparing region {:?}", region); self.renderer .prepare( device, @@ -123,12 +116,7 @@ impl TerminalText { left: 0.0, top: 0.0, scale: 1.0, - bounds: TextBounds { - left: 0, - top: 0, - right: config.width as i32, - bottom: config.height as i32, - }, + bounds: region, default_color: Color::rgb(255, 255, 255), custom_glyphs: &[], }], @@ -157,47 +145,21 @@ impl TerminalText { // TODO return; } - info!("line nr: {line}"); - let current_line = self.buffer.lines[line].text().to_string(); - let pos = self.cursor.index.min(current_line.len()); - - info!("cursor position: {pos}"); - - info!("Line text: {current_line}"); - - let mut new_text = current_line.clone(); - new_text.insert(pos, c); - - let line_bounds = self.get_line_bounds(line); - self.dirty_regions.push(line_bounds); + let mut text = self.buffer.lines[line].text().to_string().clone(); + let pos = self.cursor.index.min(text.len()); + text.insert(pos, c); let ending = self.buffer.lines[line].ending(); let attrs = self.buffer.lines[line].attrs_list().clone(); // Update the buffer with the new text - self.buffer.lines[line].set_text(&new_text, ending, attrs); - + self.buffer.lines[line].set_text(&text, ending, attrs); // Update cursor position self.cursor.index = pos + 1; self.buffer.shape_until_scroll(&mut self.font_system, false); - } - - fn get_line_bounds(&self, line: usize) -> TextBounds { - // Calculate the bounds for a specific line - // This is a simplified version - you'll need to implement this based on your text layout - TextBounds { - left: 0, - top: (line * self.get_line_height()) as i32, - right: self.viewport.resolution().width as i32, - bottom: ((line + 1) * self.get_line_height()) as i32, - } - } - - fn get_line_height(&self) -> usize { - // Get the line height based on your font metrics - self.buffer.metrics().line_height.ceil() as usize + self.ensure_visible_text_rendered(); } pub fn handle_enter(&mut self) { @@ -223,53 +185,119 @@ impl TerminalText { // Create a new line with text after cursor let new_line = BufferLine::new(text_after, ending, attrs, Shaping::Advanced); - - // Insert the new line after the current one self.buffer.lines.insert(line + 1, new_line); - self.mark_all_dirty(); - // Update cursor position to beginning of next line self.cursor.line = line + 1; self.cursor.index = 0; // Reshape the buffer self.buffer.shape_until_scroll(&mut self.font_system, false); + self.ensure_visible_text_rendered(); } - pub fn mark_all_dirty(&mut self) { - // Create a single region covering the entire viewport - let full_bounds = TextBounds { - left: 0, - top: 0, - right: self.viewport.resolution().width as i32, - bottom: self.viewport.resolution().height as i32, + fn get_line_bounds(&self, line: usize) -> TextBounds { + // Calculate the bounds for a specific line + + let line_height = self.buffer.metrics().line_height; + let viewport_width = self.viewport.resolution().width; + + // Calculate floating point values first + let top_f32 = (line as f32 * line_height).floor(); + let bottom_f32 = ((line as f32 + 1.0) * line_height).ceil(); + + // Safe conversions with overflow checks + let top = if top_f32 > i32::MAX as f32 { + i32::MAX + } else if top_f32 < i32::MIN as f32 { + i32::MIN + } else { + top_f32 as i32 }; - self.dirty_regions = vec![full_bounds]; - } - pub fn move_cursor_left(&mut self) { - if self.cursor.index > 0 { - self.cursor.index -= 1; - } else if self.cursor.line > 0 { - // Move to end of previous line - self.cursor.line -= 1; - let prev_line_len = self.buffer.lines[self.cursor.line].text().len(); - self.cursor.index = prev_line_len; + let bottom = if bottom_f32 > i32::MAX as f32 { + i32::MAX + } else if bottom_f32 < i32::MIN as f32 { + i32::MIN + } else { + bottom_f32 as i32 + }; + + // Ensure viewport width doesn't exceed i32::MAX + let right = viewport_width.min(i32::MAX as u32) as i32; + + TextBounds { + left: 0, + top, + right, + bottom, } } - pub fn move_cursor_right(&mut self) { - if self.cursor.line < self.buffer.lines.len() { - let current_line_len = self.buffer.lines[self.cursor.line].text().len(); + fn get_visible_line_range(&self) -> (usize, usize) { + let viewport_height = self.viewport.resolution().height as f32; + let line_height = self.buffer.metrics().line_height; - if self.cursor.index < current_line_len { - self.cursor.index += 1; - } else if self.cursor.line < self.buffer.lines.len() - 1 { - // Move to beginning of next line - self.cursor.line += 1; - self.cursor.index = 0; - } + // Start from line 0 (no scrolling yet) + let start_line = 0; + + // Calculate how many complete lines fit in the viewport + // Add 1 to include partially visible lines at the bottom + let visible_lines = (viewport_height / line_height).ceil() as usize + 1; + + // Make sure we don't go beyond the actual number of lines + let end_line = if self.buffer.lines.is_empty() { + 0 + } else { + (start_line + visible_lines).min(self.buffer.lines.len()) + }; + + trace!("visible line range goes from {start_line} to {end_line}"); + + (start_line, end_line) + } + + pub fn ensure_visible_text_rendered(&mut self) { + let (start_line, end_line) = self.get_visible_line_range(); + for i in start_line..end_line { + self.dirty_regions.push(self.get_line_bounds(i)); } } + + // pub fn mark_all_dirty(&mut self) { + // trace!("Marking all regions as dirty"); + // // Create a single region covering the entire viewport + // let full_bounds = TextBounds { + // left: 0, + // top: 0, + // right: self.viewport.resolution().width as i32, + // bottom: self.viewport.resolution().height as i32, + // }; + // self.dirty_regions = vec![full_bounds]; + // } + + // pub fn move_cursor_left(&mut self) { + // if self.cursor.index > 0 { + // self.cursor.index -= 1; + // } else if self.cursor.line > 0 { + // // Move to end of previous line + // self.cursor.line -= 1; + // let prev_line_len = self.buffer.lines[self.cursor.line].text().len(); + // self.cursor.index = prev_line_len; + // } + // } + + // pub fn move_cursor_right(&mut self) { + // if self.cursor.line < self.buffer.lines.len() { + // let current_line_len = self.buffer.lines[self.cursor.line].text().len(); + + // if self.cursor.index < current_line_len { + // self.cursor.index += 1; + // } else if self.cursor.line < self.buffer.lines.len() - 1 { + // // Move to beginning of next line + // self.cursor.line += 1; + // self.cursor.index = 0; + // } + // } + // } }