diff --git a/src/box_drawing.rs b/src/box_drawing.rs index 467a9d0..1c408c9 100644 --- a/src/box_drawing.rs +++ b/src/box_drawing.rs @@ -634,13 +634,13 @@ pub fn render_box_char( } }; - let h_thick = thickness(left.max(right)); let v_thick = thickness(up.max(down)); + let h_thick = thickness(left.max(right)); let h_extend = v_thick / 2; let v_extend = h_thick / 2; if left > 0 { - hline(&mut bitmap, 0, mid_x + h_extend + 1, mid_y, thickness(left)); + hline(&mut bitmap, 0, mid_x + h_extend, mid_y, thickness(left)); } if right > 0 { hline( @@ -652,7 +652,7 @@ pub fn render_box_char( ); } if up > 0 { - vline(&mut bitmap, 0, mid_y + v_extend + 1, mid_x, thickness(up)); + vline(&mut bitmap, 0, mid_y + v_extend, mid_x, thickness(up)); } if down > 0 { vline( diff --git a/src/renderer.rs b/src/renderer.rs index ff4d9d3..55a9061 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -175,9 +175,10 @@ struct SpriteKey { impl SpriteKey { /// Create a key for a single-cell sprite (the common case) + /// Uses cell_index=255 as sentinel to distinguish from multi-cell cell 0 #[inline] fn single(ch: char, style: FontStyle, colored: bool) -> Self { - Self { ch, cell_index: 0, style, colored } + Self { ch, cell_index: 255, style, colored } } /// Create a key for a multi-cell sprite @@ -2267,6 +2268,12 @@ impl Renderer { // Regular character - create sprite as normal let (sprite_idx, is_colored) = self.get_or_create_sprite(c, style); + // DEBUG: Log colored glyph detection + if is_colored { + log::debug!("EMOJI MULTICELL CHECK: col={} char=U+{:04X} '{}' sprite_idx={} is_colored={}", + col, c as u32, c, sprite_idx, is_colored); + } + // If this is a colored glyph (emoji) followed by empty cells, create multi-cell sprites if is_colored && sprite_idx != 0 { // Count trailing empty cells for potential multi-cell emoji @@ -2276,6 +2283,8 @@ impl Renderer { while col + num_empty + 1 < row.len() && num_empty < MAX_EXTRA_CELLS { let next_cell = &row[col + num_empty + 1]; let next_char = next_cell.character; + log::debug!(" checking next cell at col={}: char=U+{:04X} '{}' wide_cont={}", + col + num_empty + 1, next_char as u32, next_char, next_cell.wide_continuation); if next_char == ' ' || next_char == '\u{2002}' || next_char == '\0' { num_empty += 1; } else { @@ -2283,17 +2292,22 @@ impl Renderer { } } + log::debug!(" found {} trailing empty cells", num_empty); + if num_empty > 0 { let total_cells = 1 + num_empty; + log::debug!(" creating multi-cell sprites for {} cells", total_cells); // Check if we already have multi-cell sprites for this emoji let first_key = SpriteKey::multi(c, 0, style, true); if self.sprite_map.get(&first_key).is_none() { - // Need to create multi-cell emoji sprites + log::debug!(" rasterizing multi-cell emoji U+{:04X}", c as u32); let cell_sprites = self.rasterize_emoji_multicell(c, total_cells); + log::debug!(" got {} cell sprites", cell_sprites.len()); for (cell_idx, glyph) in cell_sprites.into_iter().enumerate() { + log::debug!(" cell {} sprite size: {:?}", cell_idx, glyph.size); if glyph.size[0] > 0.0 && glyph.size[1] > 0.0 { let key = SpriteKey::multi(c, cell_idx as u8, style, true); diff --git a/src/vt_parser.rs b/src/vt_parser.rs index 93d185d..6cd03fc 100644 --- a/src/vt_parser.rs +++ b/src/vt_parser.rs @@ -240,8 +240,13 @@ struct BufferState { read_pos: usize, read_consumed: usize, read_sz: usize, - /// Write tracking: pending = bytes written by I/O but not yet visible to reader + /// Write tracking (like Kitty's write struct): + /// - pending: bytes written by I/O but not yet visible to reader + /// - offset: where I/O thread is writing (for compaction fixup) + /// - sz: size of current write buffer (0 if none outstanding) write_pending: usize, + write_offset: usize, + write_sz: usize, } /// Kitty-style shared parser with integrated 1MB buffer. @@ -306,6 +311,8 @@ impl SharedParser { read_consumed: 0, read_sz: 0, write_pending: 0, + write_offset: 0, + write_sz: 0, }), wakeup_fd, // Parser state - working copies for use while lock is released @@ -339,8 +346,17 @@ impl SharedParser { /// Get write buffer for I/O thread. Returns (ptr, available_bytes). /// Caller MUST call commit_write() after writing. + /// Like Kitty's vt_parser_create_write_buffer(). pub fn create_write_buffer(&self) -> (*mut u8, usize) { - let state = self.state.lock().unwrap(); + let mut state = self.state.lock().unwrap(); + + if state.write_sz > 0 { + log::error!( + "create_write_buffer called with existing write buffer" + ); + return (std::ptr::null_mut(), 0); + } + let write_offset = state.read_sz + state.write_pending; let available = BUF_SIZE.saturating_sub(write_offset); @@ -348,15 +364,33 @@ impl SharedParser { return (std::ptr::null_mut(), 0); } - // SAFETY: I/O writes past read_sz+write_pending + state.write_offset = write_offset; + state.write_sz = available; + let ptr = unsafe { (*self.buf.get()).as_mut_ptr().add(write_offset) }; (ptr, available) } /// Commit bytes written by I/O thread. + /// Like Kitty's vt_parser_commit_write() - handles compaction that happened + /// between create_write_buffer and commit_write by moving data if needed. pub fn commit_write(&self, len: usize) { let mut state = self.state.lock().unwrap(); + let current_offset = state.read_sz + state.write_pending; + + if state.write_offset > current_offset { + unsafe { + let buf = &mut *self.buf.get(); + std::ptr::copy( + buf.as_ptr().add(state.write_offset), + buf.as_mut_ptr().add(current_offset), + len, + ); + } + } + state.write_pending += len; + state.write_sz = 0; } /// Read from PTY fd into buffer. Returns bytes read, -1 for error. @@ -371,10 +405,18 @@ impl SharedParser { if result > 0 { self.commit_write(result as usize); + } else { + self.cancel_write(); } result } + /// Cancel a pending write buffer (on error/EOF). + fn cancel_write(&self) { + let mut state = self.state.lock().unwrap(); + state.write_sz = 0; + } + /// Drain the wakeup eventfd. pub fn drain_wakeup(&self) { let mut buf = 0u64; @@ -412,9 +454,6 @@ impl SharedParser { return false; } - let initial_pos = state.read_pos; - let initial_sz = state.read_sz; - // Track if buffer was ever full during this parse pass (for wakeup decision) // Like Kitty: pd->write_space_created = self->read.sz >= BUF_SZ (checked BEFORE compaction) let mut buffer_was_ever_full = state.read_sz >= BUF_SIZE; @@ -422,25 +461,14 @@ impl SharedParser { // Reset consumed counter for this parse pass (like Kitty: self->read.consumed = 0) state.read_consumed = 0; - // Check vte_state at start of parse pass - let vte_state_at_start = unsafe { *self.vte_state.get() }; - if vte_state_at_start != State::Normal { - log::error!( - "DEBUG run_parse_pass START: vte_state={:?} read_pos={} read_sz={}", - vte_state_at_start, state.read_pos, state.read_sz - ); - } - // Copy positions to UnsafeCell fields for use while lock is released unsafe { *self.parse_pos.get() = state.read_pos; *self.parse_sz.get() = state.read_sz; - *self.parse_consumed.get() = state.read_pos; // consumed starts at current pos + *self.parse_consumed.get() = state.read_pos; } - // Parse loop - release lock during parsing! // Like Kitty's do { ... } while (self->read.pos < self->read.sz) - let mut loop_count = 0; loop { let parse_pos = unsafe { *self.parse_pos.get() }; let parse_sz = unsafe { *self.parse_sz.get() }; @@ -452,10 +480,9 @@ impl SharedParser { // RELEASE LOCK during parsing - I/O can continue writing! drop(state); - // Parse the data - consume_input updates parse_pos and parse_consumed - self.consume_input(handler); + // Like Kitty line 1516: consume_input(self, ...) + let made_progress = self.consume_input(handler); parsed_any = true; - loop_count += 1; // Re-acquire lock state = self.state.lock().unwrap(); @@ -479,45 +506,14 @@ impl SharedParser { *self.parse_sz.get() = state.read_sz; } - // If no more unparsed data, we're done (like Kitty: while read.pos < read.sz) - if state.read_pos >= state.read_sz { + // Like Kitty: while read.pos < read.sz + if state.read_pos >= state.read_sz || !made_progress { break; } } - let bytes_parsed = state.read_pos.saturating_sub(initial_pos); - if bytes_parsed > 0 || loop_count > 1 { - log::debug!("[PARSE] initial_pos={} initial_sz={} final_pos={} final_sz={} loops={} bytes={}", - initial_pos, initial_sz, state.read_pos, state.read_sz, loop_count, bytes_parsed); - } - // Compaction - remove consumed bytes (like Kitty) if state.read_consumed > 0 { - let old_sz = state.read_sz; - - // Debug: Check what we're about to discard and what state we're in - let vte_state = unsafe { *self.vte_state.get() }; - if vte_state != State::Normal { - let buf = unsafe { &*self.buf.get() }; - let remaining_start = state.read_consumed.min(old_sz); - let preview_len = (old_sz - remaining_start).min(20); - let preview: String = buf - [remaining_start..remaining_start + preview_len] - .iter() - .map(|&b| { - if b >= 0x20 && b < 0x7f { - b as char - } else { - '.' - } - }) - .collect(); - log::error!( - "DEBUG COMPACT: state={:?} consumed={} old_sz={} remaining preview: {:?}", - vte_state, state.read_consumed, old_sz, preview - ); - } - // Like Kitty: pos -= consumed, sz -= consumed, memmove state.read_pos = state.read_pos.saturating_sub(state.read_consumed); state.read_sz = state.read_sz.saturating_sub(state.read_consumed); @@ -534,14 +530,11 @@ impl SharedParser { } } - let consumed = state.read_consumed; state.read_consumed = 0; // Wake I/O thread if buffer was ever full during this pass and we freed space // Like Kitty: if (pd.write_space_created) wakeup_io_loop() if buffer_was_ever_full && state.read_sz < BUF_SIZE { - log::debug!("[PARSE] Waking I/O: was_full={} old_sz={} new_sz={} consumed={}", - buffer_was_ever_full, old_sz, state.read_sz, consumed); drop(state); let val = 1u64; unsafe { @@ -572,16 +565,14 @@ impl SharedParser { // ========== Internal parsing methods (main thread only) ========== /// Main parsing dispatch - like Kitty's consume_input(). - /// Reads from buf[parse_pos..parse_sz] and updates positions. + /// Processes ONE state case per call and returns, like Kitty lines 1458-1490. + /// The outer loop in run_parse_pass() calls this repeatedly until buffer exhausted. /// - /// IMPORTANT: Unlike the previous implementation, this now loops internally - /// until the buffer is exhausted or we're waiting for more data in an incomplete - /// escape sequence. This reduces per-CSI overhead from 3 function calls to 1. - fn consume_input(&self, handler: &mut H) { + /// Returns true if we made progress (consumed some bytes or changed state). + fn consume_input(&self, handler: &mut H) -> bool { #[cfg(feature = "render_timing")] let start = std::time::Instant::now(); - // Get mutable access to parser state (SAFETY: only main thread calls this) let parse_pos = unsafe { &mut *self.parse_pos.get() }; let parse_sz = unsafe { *self.parse_sz.get() }; let parse_consumed = unsafe { &mut *self.parse_consumed.get() }; @@ -594,131 +585,119 @@ impl SharedParser { let escape_len = unsafe { &mut *self.escape_len.get() }; let buf = unsafe { &*self.buf.get() }; - // Debug: Log state at start of consume_input - if *vte_state != State::Normal { - log::error!( - "DEBUG consume_input START: state={:?} pos={} consumed={} sz={}", - *vte_state, *parse_pos, *parse_consumed, parse_sz - ); + if *parse_pos >= parse_sz { + #[cfg(feature = "render_timing")] + handler.add_vt_parser_ns(start.elapsed().as_nanos() as u64); + return false; } - // Debug: Check if we're starting in Normal with CSI-like content - if *vte_state == State::Normal - && *parse_pos < parse_sz - && buf[*parse_pos] == b'[' - { - log::error!( - "DEBUG: Starting consume_input in Normal but buf[{}]='[' (0x5b). consumed={} sz={}", - *parse_pos, *parse_consumed, parse_sz - ); - } - - // Loop until buffer exhausted or waiting for more data - while *parse_pos < parse_sz { - match *vte_state { - State::Normal => { - Self::consume_normal_impl( - handler, - buf, - parse_pos, - parse_sz, - utf8, - codepoint_buf, - vte_state, - escape_len, - ); + let made_progress = match *vte_state { + State::Normal => { + // Like Kitty line 1460: consume_normal(self); self->read.consumed = self->read.pos; break; + Self::consume_normal_impl( + handler, + buf, + parse_pos, + parse_sz, + utf8, + codepoint_buf, + vte_state, + escape_len, + ); + *parse_consumed = *parse_pos; + true + } + State::Escape => { + // Like Kitty lines 1461-1463: + // case VTE_ESC: if (consume_esc(self)) { self->read.consumed = self->read.pos; } break; + if Self::consume_escape_impl( + handler, + buf, + parse_pos, + parse_sz, + *parse_consumed, + vte_state, + csi, + osc_buffer, + string_buffer, + escape_len, + ) { *parse_consumed = *parse_pos; } - State::Escape => { - let state_before = *vte_state; - if Self::consume_escape_impl( - handler, - buf, - parse_pos, - parse_sz, - *parse_consumed, - vte_state, - csi, - osc_buffer, - string_buffer, - escape_len, - ) { - *parse_consumed = *parse_pos; - } else if *vte_state != state_before { - // State changed to multi-byte sequence (CSI, OSC, etc.) - // Continue loop WITHOUT updating parse_consumed - } else { - // Need more data for escape sequence - break; - } + true + } + State::EscapeIntermediate(_) => { + if Self::consume_escape_intermediate_impl( + handler, buf, parse_pos, parse_sz, vte_state, + ) { + *parse_consumed = *parse_pos; } - State::EscapeIntermediate(_) => { - if Self::consume_escape_intermediate_impl( - handler, buf, parse_pos, parse_sz, vte_state, - ) { - *parse_consumed = *parse_pos; - } else { - break; - } - } - State::Csi => { - // Like Kitty: if (consume_csi(self)) { self->read.consumed = self->read.pos; dispatch; SET_STATE(NORMAL); } - if Self::consume_csi_impl( - handler, - buf, - parse_pos, - parse_sz, - *parse_consumed, - csi, - escape_len, - ) { - *parse_consumed = *parse_pos; - if csi.is_valid { - handler.csi(csi); - } - *vte_state = State::Normal; - // Continue loop to process more data - } else { - // Need more data for CSI sequence - break; - } - } - State::Osc => { - if Self::consume_osc_impl( - handler, buf, parse_pos, parse_sz, vte_state, - osc_buffer, escape_len, - ) { - *parse_consumed = *parse_pos; - *vte_state = State::Normal; - } else { - break; - } - } - State::Dcs | State::Apc | State::Pm | State::Sos => { - if Self::consume_string_impl( - handler, - buf, - parse_pos, - parse_sz, - vte_state, - string_buffer, - escape_len, - ) { - *parse_consumed = *parse_pos; - *vte_state = State::Normal; - } else { - break; + true + } + State::Csi => { + // Like Kitty lines 1465-1466: + // if (consume_csi(self)) { self->read.consumed = self->read.pos; if (self->csi.is_valid) dispatch_csi(self); SET_STATE(NORMAL); } + if Self::consume_csi_impl( + handler, + buf, + parse_pos, + parse_sz, + *parse_consumed, + csi, + escape_len, + ) { + *parse_consumed = *parse_pos; + if csi.is_valid { + handler.csi(csi); } + *vte_state = State::Normal; + true + } else { + false } } - } + State::Osc => { + if Self::consume_osc_impl( + handler, buf, parse_pos, parse_sz, vte_state, osc_buffer, + escape_len, + ) { + *parse_consumed = *parse_pos; + *vte_state = State::Normal; + true + } else { + false + } + } + State::Dcs | State::Apc | State::Pm | State::Sos => { + if Self::consume_string_impl( + handler, + buf, + parse_pos, + parse_sz, + vte_state, + string_buffer, + escape_len, + ) { + *parse_consumed = *parse_pos; + *vte_state = State::Normal; + true + } else { + false + } + } + }; #[cfg(feature = "render_timing")] handler.add_vt_parser_ns(start.elapsed().as_nanos() as u64); + + made_progress } /// Consume normal text - like Kitty's consume_normal(). /// UTF-8 decodes until ESC is found using SIMD-optimized decoder. + /// + /// Like Kitty: processes text until ESC found, then sets state to Escape and returns. + /// The outer loop will call consume_input again to handle the Escape state. #[inline] fn consume_normal_impl( handler: &mut H, @@ -730,6 +709,7 @@ impl SharedParser { vte_state: &mut State, escape_len: &mut usize, ) { + // Like Kitty's consume_normal() inner loop loop { if *parse_pos >= parse_sz { break; @@ -738,18 +718,23 @@ impl SharedParser { let remaining = &buf[*parse_pos..parse_sz]; let (consumed, found_esc) = utf8.decode_to_esc(remaining, codepoint_buf); - *parse_pos += consumed; if !codepoint_buf.is_empty() { handler.text(codepoint_buf); } + // Like Kitty: self->read.pos += self->utf8_decoder.num_consumed + *parse_pos += consumed; + if found_esc { + // Like Kitty: if (sentinel_found) { SET_STATE(ESC); break; } *vte_state = State::Escape; *escape_len = 0; break; } } + // Like Kitty line 1460: consume_normal(self); self->read.consumed = self->read.pos; break; + // The caller (consume_input) will set parse_consumed = parse_pos } /// Consume escape sequence start - like Kitty's consume_esc(). @@ -786,33 +771,28 @@ impl SharedParser { b'[' => { *vte_state = State::Csi; csi.reset(); - return false; } b']' => { *vte_state = State::Osc; osc_buffer.clear(); - return false; } b'P' => { *vte_state = State::Dcs; string_buffer.clear(); - return false; } b'_' => { *vte_state = State::Apc; string_buffer.clear(); - return false; } b'^' => { *vte_state = State::Pm; string_buffer.clear(); - return false; } b'X' => { *vte_state = State::Sos; string_buffer.clear(); - return false; } + // Two-byte escape sequences - return false like Kitty's IS_ESCAPED_CHAR b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/' | b'%' | b'#' | b' ' => { *vte_state = State::EscapeIntermediate(ch);