diff --git a/src/gpu_types.rs b/src/gpu_types.rs index 111a53d..6f8a7d3 100644 --- a/src/gpu_types.rs +++ b/src/gpu_types.rs @@ -4,6 +4,19 @@ //! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility. use bytemuck::{Pod, Zeroable}; +use std::sync::atomic::{AtomicU64, Ordering}; + +/// Unique identifier for a pane. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct PaneId(pub u64); + +impl PaneId { + /// Generate a new unique pane ID. + pub fn new() -> Self { + static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + Self(COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)) + } +} // ═══════════════════════════════════════════════════════════════════════════════ // CONSTANTS @@ -148,8 +161,8 @@ pub struct ImageUniforms { pub src_y: f32, pub src_width: f32, pub src_height: f32, + pub dim_factor: f32, pub _padding1: f32, - pub _padding2: f32, } // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/graphics.rs b/src/graphics.rs index 4933109..17eebec 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -137,10 +137,18 @@ pub struct GraphicsCommand { pub delete_target: DeleteTarget, /// Unicode placeholder (virtual placement). pub unicode_placeholder: bool, + /// Parent image ID (for relative placement). + pub parent_image_id: Option, + /// Parent placement ID (for relative placement). + pub parent_placement_id: Option, + /// Horizontal cell displacement from parent. + pub h_offset: i32, + /// Vertical cell displacement from parent. + pub v_offset: i32, /// Parent image ID (for animation frames). pub parent_id: Option, /// Parent placement ID (for animation frames). - pub parent_placement_id: Option, + pub parent_placement_id_anim: Option, /// Frame number (for animation). pub frame_number: Option, /// Frame gap in milliseconds (z key for animation frames). @@ -256,6 +264,17 @@ impl GraphicsCommand { cmd.height = value.parse().ok(); } } + "P" => { + if is_animation { + // P = parent_frame_index for animation + cmd.base_frame = value.parse().ok(); + } else { + cmd.parent_image_id = value.parse().ok(); + } + } + "Q" => cmd.parent_placement_id = value.parse().ok(), + "H" => cmd.h_offset = value.parse().unwrap_or(0), + "V" => cmd.v_offset = value.parse().unwrap_or(0), "x" => cmd.src_x = value.parse().unwrap_or(0), "y" => cmd.src_y = value.parse().unwrap_or(0), "w" => cmd.src_width = value.parse().unwrap_or(0), @@ -1611,6 +1630,19 @@ impl ImageStorage { cmd.rows as usize }; + // Handle relative positioning + let (final_col, final_row) = if let (Some(p_id), Some(q_id)) = (cmd.parent_image_id, cmd.parent_placement_id) { + if let Some(parent) = self.placements.iter().find(|p| p.image_id == p_id && p.placement_id == q_id) { + let col = parent.col as i32 + cmd.h_offset; + let row = parent.row as i32 + cmd.v_offset; + (col.max(0) as usize, row.max(0) as usize) + } else { + (cursor_col, cursor_row) + } + } else { + (cursor_col, cursor_row) + }; + // Don't create actual placement for virtual placements (U=1) // Virtual placements are referenced by Unicode placeholders if cmd.unicode_placeholder { @@ -1626,8 +1658,8 @@ impl ImageStorage { let placement = ImagePlacement { image_id: id, placement_id: cmd.placement_id.unwrap_or(0), - col: cursor_col, - row: cursor_row, + col: final_col, + row: final_row, cols, rows, z_index: cmd.z_index, diff --git a/src/image_renderer.rs b/src/image_renderer.rs index df75ebe..5e77997 100644 --- a/src/image_renderer.rs +++ b/src/image_renderer.rs @@ -4,7 +4,7 @@ //! supporting the Kitty Graphics Protocol for inline image display. use std::collections::HashMap; -use crate::gpu_types::ImageUniforms; +use crate::gpu_types::{ImageUniforms, PaneId}; use crate::graphics::{ImageData, ImagePlacement, ImageStorage}; // ═══════════════════════════════════════════════════════════════════════════════ @@ -15,7 +15,6 @@ use crate::graphics::{ImageData, ImagePlacement, ImageStorage}; pub struct GpuImage { pub texture: wgpu::Texture, pub view: wgpu::TextureView, - pub uniform_buffer: wgpu::Buffer, pub bind_group: wgpu::BindGroup, pub width: u32, pub height: u32, @@ -28,14 +27,23 @@ pub struct GpuImage { /// Manages GPU resources for image rendering. /// Handles uploading, caching, and preparing images for rendering. pub struct ImageRenderer { - /// Bind group layout for image rendering. - bind_group_layout: wgpu::BindGroupLayout, + /// Bind group layout for uniforms. + uniform_layout: wgpu::BindGroupLayout, + /// Bind group layout for textures. + texture_layout: wgpu::BindGroupLayout, /// Sampler for image textures. sampler: wgpu::Sampler, - /// Cached GPU textures for images, keyed by image ID. - textures: HashMap, + /// Cached GPU textures for images, keyed by (pane_id, image_id). + textures: HashMap<(PaneId, u32), GpuImage>, + /// Global uniform buffer for image renders. + pub uniform_buffer: wgpu::Buffer, + /// Bind group for image uniforms. + uniform_bind_group: wgpu::BindGroup, + /// Minimum offset alignment for uniform buffers. + pub alignment: u64, } + impl ImageRenderer { /// Create a new ImageRenderer with the necessary GPU resources. pub fn new(device: &wgpu::Device) -> Self { @@ -51,20 +59,25 @@ impl ImageRenderer { ..Default::default() }); - // Create bind group layout for images - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Image Bind Group Layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, + // Create bind group layout for uniforms (binding 0) + let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Image Uniform Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: None, }, + count: None, + }], + }); + + // Create bind group layout for textures (binding 1, 2) + let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Image Texture Layout"), + entries: &[ wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, @@ -84,31 +97,71 @@ impl ImageRenderer { ], }); + // Create a large uniform buffer for all image renders in a frame + // Max 256 images per frame (65536 / 256) + let buffer_size = 65536; + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Image Uniform Buffer"), + size: buffer_size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Create the uniform bind group + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Image Uniform Bind Group"), + layout: &uniform_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &uniform_buffer, + offset: 0, + size: std::num::NonZeroU64::new(std::mem::size_of::() as u64), + }), + }], + }); + + let alignment = device.limits().min_uniform_buffer_offset_alignment as u64; + Self { - bind_group_layout, + uniform_layout, + texture_layout, sampler, textures: HashMap::new(), + uniform_buffer, + uniform_bind_group, + alignment, } } - /// Get the bind group layout for creating the image pipeline. - pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout { - &self.bind_group_layout + /// Get the uniform bind group layout. + pub fn uniform_layout(&self) -> &wgpu::BindGroupLayout { + &self.uniform_layout + } + + /// Get the texture bind group layout. + pub fn texture_layout(&self) -> &wgpu::BindGroupLayout { + &self.texture_layout + } + + /// Get the uniform bind group. + pub fn uniform_bind_group(&self) -> &wgpu::BindGroup { + &self.uniform_bind_group } /// Get a GPU image by ID. - pub fn get(&self, image_id: &u32) -> Option<&GpuImage> { - self.textures.get(image_id) + pub fn get(&self, pane_id: PaneId, image_id: &u32) -> Option<&GpuImage> { + self.textures.get(&(pane_id, *image_id)) } /// Upload an image to the GPU, creating or updating its texture. - pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, image: &ImageData) { - log::debug!("upload_image: id={}, width={}, height={}, data_len={}", image.id, image.width, image.height, image.data.len()); + pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, pane_id: PaneId, image: &ImageData) { + log::debug!("upload_image: pane_id={:?}, id={}, width={}, height={}, data_len={}", pane_id, image.id, image.width, image.height, image.data.len()); // Get current frame data (handles animation frames automatically) let data = image.current_frame_data(); // Check if we already have this image - if let Some(existing) = self.textures.get(&image.id) { + if let Some(existing) = self.textures.get(&(pane_id, image.id)) { if existing.width == image.width && existing.height == image.height { // Same dimensions, just update the data queue.write_texture( @@ -137,7 +190,7 @@ impl ImageRenderer { // Create new texture let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some(&format!("Image {}", image.id)), + label: Some(&format!("Image {} (pane {:?})", image.id, pane_id)), size: wgpu::Extent3d { width: image.width, height: image.height, @@ -174,23 +227,10 @@ impl ImageRenderer { let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - // Create per-image uniform buffer - let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some(&format!("Image {} Uniform Buffer", image.id)), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - // Create bind group for this image with its own uniform buffer let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some(&format!("Image {} Bind Group", image.id)), - layout: &self.bind_group_layout, + label: Some(&format!("Image {} (pane {:?}) Bind Group", image.id, pane_id)), + layout: &self.texture_layout, entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.as_entire_binding(), - }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&view), @@ -201,16 +241,16 @@ impl ImageRenderer { }, ], }); - - self.textures.insert(image.id, GpuImage { + + self.textures.insert((pane_id, image.id), GpuImage { texture, view, - uniform_buffer, bind_group, width: image.width, height: image.height, }); + log::debug!( "Uploaded image {} ({}x{}) to GPU", image.id, @@ -220,52 +260,61 @@ impl ImageRenderer { } /// Remove an image from the GPU. - pub fn remove_image(&mut self, image_id: u32) { - if self.textures.remove(&image_id).is_some() { - log::debug!("Removed image {} from GPU", image_id); + pub fn remove_image(&mut self, pane_id: PaneId, image_id: u32) { + if self.textures.remove(&(pane_id, image_id)).is_some() { + log::debug!("Removed image {} (pane {:?}) from GPU", image_id, pane_id); } } /// Sync images from terminal's image storage to GPU. /// Uploads new/changed images and removes deleted ones. /// Also updates animation frames. - pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) { + pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, pane_id: PaneId, storage: &mut ImageStorage) { // Update animations and get list of changed image IDs let changed_ids = storage.update_animations(); - log::debug!("Sync images: changed_ids={:?}, dirty={}", changed_ids, storage.dirty); - + log::debug!("Sync images: pane_id={:?}, changed_ids={:?}, dirty={}", pane_id, changed_ids, storage.dirty); + // Re-upload frames that changed due to animation for id in &changed_ids { if let Some(image) = storage.get_image(*id) { - self.upload_image(device, queue, image); + self.upload_image(device, queue, pane_id, image); } } - + if !storage.dirty && changed_ids.is_empty() { return; } - + // Upload all images (upload_image handles deduplication) for image in storage.images().values() { - self.upload_image(device, queue, image); + self.upload_image(device, queue, pane_id, image); } + + storage.clear_dirty(); + } - // Remove textures for deleted images - let current_ids: std::collections::HashSet = storage.images().keys().copied().collect(); - let gpu_ids: Vec = self.textures.keys().copied().collect(); + + /// Remove images from the GPU that are not present in the provided set of active images. + /// active_images is a set of (pane_id, image_id) tuples. + pub fn gc_images(&mut self, active_images: &std::collections::HashSet<(PaneId, u32)>) { + let gpu_ids: Vec<(PaneId, u32)> = self.textures.keys().copied().collect(); + let mut removed_count = 0; for id in gpu_ids { - if !current_ids.contains(&id) { - self.remove_image(id); + if !active_images.contains(&id) { + self.remove_image(id.0, id.1); + removed_count += 1; } } - - storage.clear_dirty(); + if removed_count > 0 { + log::debug!("GC images: removed {} unused textures", removed_count); + } } /// Prepare image renders for a pane. /// Returns a Vec of (image_id, uniforms) for deferred rendering. pub fn prepare_image_renders( &self, + pane_id: PaneId, placements: &[ImagePlacement], pane_x: f32, pane_y: f32, @@ -276,14 +325,19 @@ impl ImageRenderer { scrollback_len: usize, scroll_offset: usize, visible_rows: usize, + dim_factor: f32, ) -> Vec<(u32, ImageUniforms)> { + log::debug!("prepare_image_renders: pane={:?}, placements={}, scrollback={}, offset={}, rows={}", pane_id, placements.len(), scrollback_len, scroll_offset, visible_rows); let mut renders = Vec::new(); for placement in placements { // Check if we have the GPU texture for this image - let gpu_image = match self.textures.get(&placement.image_id) { + let gpu_image = match self.textures.get(&(pane_id, placement.image_id)) { Some(img) => img, - None => continue, // Skip if not uploaded yet + None => { + log::debug!("Image {} not found in GPU cache for pane {:?}", placement.image_id, pane_id); + continue; + }, }; // Convert absolute row to visible screen row @@ -338,8 +392,8 @@ impl ImageRenderer { src_y, src_width, src_height, + dim_factor, _padding1: 0.0, - _padding2: 0.0, }; renders.push((placement.image_id, uniforms)); diff --git a/src/image_shader.wgsl b/src/image_shader.wgsl index 9cae00b..1d4c13c 100644 --- a/src/image_shader.wgsl +++ b/src/image_shader.wgsl @@ -16,18 +16,19 @@ struct ImageUniforms { src_y: f32, src_width: f32, src_height: f32, + // Dim factor for unfocused panes (1.0 = bright, 0.0 = dimmed) + dim_factor: f32, // Padding for alignment _padding1: f32, - _padding2: f32, } @group(0) @binding(0) var uniforms: ImageUniforms; -@group(0) @binding(1) +@group(1) @binding(1) var image_texture: texture_2d; -@group(0) @binding(2) +@group(1) @binding(2) var image_sampler: sampler; struct VertexOutput { @@ -87,6 +88,9 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { // Sample the image texture let color = textureSample(image_texture, image_sampler, in.uv); + // Apply dimming factor to RGB channels + let dimmed_rgb = color.rgb * uniforms.dim_factor; + // Return with premultiplied alpha for proper blending - return vec4(color.rgb * color.a, color.a); + return vec4(dimmed_rgb * color.a, color.a); } diff --git a/src/main.rs b/src/main.rs index 9514d21..0cfb572 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ //! Single-process architecture: owns PTY, terminal state, and rendering. //! Supports window close/reopen without losing terminal state. +use zterm::graphics::ImageStorage; +use zterm::vt_parser::SharedParser; use zterm::config::{Action, Config}; use zterm::keyboard::{ FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers, @@ -15,7 +17,7 @@ use zterm::renderer::{ use zterm::terminal::{ Direction, MouseTrackingMode, Terminal, TerminalCommand, }; -use zterm::vt_parser::SharedParser; +use zterm::gpu_types::PaneId; use std::collections::HashMap; use std::io::Write; @@ -40,17 +42,9 @@ use winit::keyboard::{Key, NamedKey}; use winit::platform::wayland::EventLoopBuilderExtWayland; use winit::window::{Window, WindowId}; -/// Unique identifier for a pane. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct PaneId(u64); - -impl PaneId { - fn new() -> Self { - use std::sync::atomic::AtomicU64; - static NEXT_ID: AtomicU64 = AtomicU64::new(1); - Self(NEXT_ID.fetch_add(1, Ordering::Relaxed)) - } -} +// ═══════════════════════════════════════════════════════════════════════════════ +// PANE +// ═══════════════════════════════════════════════════════════════════════════════ /// A single pane containing a terminal and its PTY. struct Pane { @@ -2026,9 +2020,12 @@ impl App { // Check if any images have running animations let image_animation_in_progress = tab.panes.values().any(|p| { p.terminal.image_storage.has_animations() + || p.terminal.alternate_screen.as_ref().map_or(false, |alt| alt.image_storage.has_animations()) }); - if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw && !image_animation_in_progress { + let has_dirty_terminal = tab.panes.values().any(|p| p.terminal.dirty); + + if !has_dirty_content && !has_dirty_terminal && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw && !image_animation_in_progress { return false; } @@ -2046,12 +2043,31 @@ impl App { ); dim_factors.push((*pane_id, dim_factor)); - // Sync terminal images to GPU (Kitty graphics protocol) - renderer.sync_images(&mut pane.terminal.image_storage); - } - } + // Sync terminal images to GPU (Kitty graphics protocol) + let storage = if pane.terminal.using_alternate_screen { + pane.terminal.alternate_screen.as_mut().map(|alt| &mut alt.image_storage).unwrap_or(&mut pane.terminal.image_storage) + } else { + &mut pane.terminal.image_storage + }; + renderer.sync_images(*pane_id, storage); + } + } + + // Garbage collect unused images across all panes + let mut image_storages = Vec::new(); + for (id, _) in &geometries { + if let Some(pane) = tab.panes.get(id) { + image_storages.push((*id, &pane.terminal.image_storage)); + if let Some(alt) = &pane.terminal.alternate_screen { + image_storages.push((*id, &alt.image_storage)); + } + } + } + renderer.gc_images(&image_storages); + + + // Clear custom statusline if the foreground process is no longer neovim/vim - // Clear custom statusline if the foreground process is no longer neovim/vim if let Some(pane) = tab.panes.get_mut(&active_pane_id) { if pane.custom_statusline.is_some() { if let Some(proc_name) = @@ -2171,8 +2187,9 @@ impl App { // Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean) for (pane_id, _) in &geometries { if let Some(pane) = tab.panes.get_mut(pane_id) { - pane.terminal.clear_dirty_lines(); - } + pane.terminal.clear_dirty_lines(); + pane.terminal.dirty = false; + } } // Clear pending redraw and needs_redraw if let Some(renderer) = &mut self.renderer { diff --git a/src/renderer.rs b/src/renderer.rs index 4ce2d39..860f1b7 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -798,7 +798,10 @@ impl Renderer { // Create pipeline layout for images let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Image Pipeline Layout"), - bind_group_layouts: &[image_renderer.bind_group_layout()], + bind_group_layouts: &[ + image_renderer.uniform_layout(), + image_renderer.texture_layout(), + ], immediate_size: 0, }); @@ -4903,25 +4906,50 @@ impl Renderer { // ═══════════════════════════════════════════════════════════════════ // PREPARE IMAGE RENDERS (Kitty Graphics Protocol) // ═══════════════════════════════════════════════════════════════════ - let mut image_renders: Vec<(u32, ImageUniforms)> = Vec::new(); + let mut image_renders: Vec<(crate::gpu_types::PaneId, u32, u64, [u32; 4])> = Vec::new(); + let mut current_uniform_offset = 0; + for (terminal, info, _) in panes { // Apply grid centering offsets to pane position let pane_x = grid_x_offset + info.x; let pane_y = terminal_y_offset + grid_y_offset + info.y; + // Compute scissor rect to clip image to pane boundaries + let scissor_x = (pane_x.round() as i32).max(0) as u32; + let scissor_y = (pane_y.round() as i32).max(0) as u32; + let scissor_w = (info.width.round() as i32).max(0).min((width as i32) - (scissor_x as i32)) as u32; + let scissor_h = (info.height.round() as i32).max(0).min((height as i32) - (scissor_y as i32)) as u32; + let scissor = [scissor_x, scissor_y, scissor_w, scissor_h]; + let renders = self.image_renderer.prepare_image_renders( - terminal.image_storage.placements(), + crate::gpu_types::PaneId(info.pane_id), + if terminal.using_alternate_screen { + terminal.alternate_screen.as_ref().map(|alt| alt.image_storage.placements()).unwrap_or_default() + } else { + terminal.image_storage.placements() + }, pane_x, pane_y, self.cell_metrics.cell_width as f32, self.cell_metrics.cell_height as f32, width, height, - terminal.scrollback.len(), - terminal.scroll_offset, + if terminal.using_alternate_screen { 0 } else { terminal.scrollback.len() }, + if terminal.using_alternate_screen { 0 } else { terminal.scroll_offset }, info.rows, + info.dim_factor, ); - image_renders.extend(renders); + for (id, uniforms) in renders { + self.queue.write_buffer( + &self.image_renderer.uniform_buffer, + current_uniform_offset, + bytemuck::cast_slice(&[uniforms]), + ); + image_renders.push((crate::gpu_types::PaneId(info.pane_id), id, current_uniform_offset, scissor)); + + // Align offset to device's min_uniform_buffer_offset_alignment + current_uniform_offset += self.image_renderer.alignment; + } } // ═══════════════════════════════════════════════════════════════════ @@ -5164,16 +5192,9 @@ impl Renderer { // IMAGE PASS (Kitty Graphics Protocol images, after glyph rendering) // Each image is rendered with its own draw call using separate bind groups // ═══════════════════════════════════════════════════════════════════ - for (image_id, uniforms) in &image_renders { + for (pane_id, image_id, offset, scissor) in &image_renders { // Check if we have the GPU texture for this image - if let Some(gpu_image) = self.image_renderer.get(image_id) { - // Upload uniforms to this image's dedicated uniform buffer - self.queue.write_buffer( - &gpu_image.uniform_buffer, - 0, - bytemuck::cast_slice(&[*uniforms]), - ); - + if let Some(gpu_image) = self.image_renderer.get(*pane_id, image_id) { // Create a render pass for this image (load existing content) let mut image_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Image Pass"), @@ -5192,8 +5213,10 @@ impl Renderer { multiview_mask: None, }); + image_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); image_pass.set_pipeline(&self.image_pipeline); - image_pass.set_bind_group(0, &gpu_image.bind_group, &[]); + image_pass.set_bind_group(0, self.image_renderer.uniform_bind_group(), &[*offset as u32]); + image_pass.set_bind_group(1, &gpu_image.bind_group, &[]); image_pass.draw(0..4, 0..1); // Triangle strip quad } } @@ -5245,10 +5268,21 @@ impl Renderer { } /// Sync images from terminal's image storage to GPU. - /// Uploads new/changed images and removes deleted ones. + /// Uploads new/changed images. /// Also updates animation frames. - pub fn sync_images(&mut self, storage: &mut ImageStorage) { - self.image_renderer.sync_images(&self.device, &self.queue, storage); + pub fn sync_images(&mut self, pane_id: crate::gpu_types::PaneId, storage: &mut ImageStorage) { + self.image_renderer.sync_images(&self.device, &self.queue, pane_id, storage); + } + + /// Remove images from the GPU that are not present in any of the provided storages. + pub fn gc_images(&mut self, storages: &[(crate::gpu_types::PaneId, &ImageStorage)]) { + let mut active_ids = std::collections::HashSet::new(); + for (pane_id, storage) in storages { + for id in storage.images().keys() { + active_ids.insert((*pane_id, *id)); + } + } + self.image_renderer.gc_images(&active_ids); } } diff --git a/src/terminal.rs b/src/terminal.rs index 88503d3..81e8966 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -287,8 +287,7 @@ struct SavedCursor { } /// Alternate screen buffer state. -#[derive(Clone)] -struct AlternateScreen { +pub struct AlternateScreen { grid: Vec>, line_map: Vec, cursor_col: usize, @@ -296,6 +295,7 @@ struct AlternateScreen { saved_cursor: SavedCursor, scroll_top: usize, scroll_bottom: usize, + pub image_storage: ImageStorage, } /// Kitty-style ring buffer for scrollback history. @@ -472,7 +472,7 @@ pub struct Terminal { /// Saved cursor state (DECSC/DECRC). saved_cursor: SavedCursor, /// Alternate screen buffer (for fullscreen apps like vim, less). - alternate_screen: Option, + pub alternate_screen: Option, /// Whether we're currently using the alternate screen. pub using_alternate_screen: bool, /// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [. @@ -775,16 +775,19 @@ impl Terminal { return; // Already in alternate screen } - // Save main screen state - self.alternate_screen = Some(AlternateScreen { - grid: self.grid.clone(), - line_map: self.line_map.clone(), - cursor_col: self.cursor_col, - cursor_row: self.cursor_row, - saved_cursor: self.saved_cursor.clone(), - scroll_top: self.scroll_top, - scroll_bottom: self.scroll_bottom, - }); + // Create alternate screen if it doesn't exist, otherwise reuse it + if self.alternate_screen.is_none() { + self.alternate_screen = Some(AlternateScreen { + grid: self.grid.clone(), + line_map: self.line_map.clone(), + cursor_col: self.cursor_col, + cursor_row: self.cursor_row, + saved_cursor: self.saved_cursor.clone(), + scroll_top: self.scroll_top, + scroll_bottom: self.scroll_bottom, + image_storage: ImageStorage::new(), + }); + } // Clear the screen for alternate buffer self.grid = vec![vec![Cell::default(); self.cols]; self.rows]; @@ -812,10 +815,10 @@ impl Terminal { return; // Not in alternate screen } - if let Some(saved) = self.alternate_screen.take() { - self.grid = saved.grid; - self.line_map = saved.line_map; - self.saved_cursor = saved.saved_cursor; + if let Some(saved) = self.alternate_screen.as_ref() { + self.grid = saved.grid.clone(); + self.line_map = saved.line_map.clone(); + self.saved_cursor = saved.saved_cursor.clone(); self.scroll_top = saved.scroll_top; self.scroll_bottom = saved.scroll_bottom; // Clamp cursor positions to current grid dimensions (defensive) @@ -2721,51 +2724,76 @@ impl Terminal { // Convert cursor_row to absolute row (accounting for scrollback) // This allows images to scroll with terminal content - let absolute_row = self.scrollback.len() + self.cursor_row; + let absolute_row = if self.using_alternate_screen { + self.cursor_row + } else { + self.scrollback.len() + self.cursor_row + }; - // Process the command - let (response, placement_result) = + log::debug!( + "Routing image command to {}: cursor_col={}, absolute_row={}, using_alt={}", + if self.using_alternate_screen { "alternate" } else { "main" }, + self.cursor_col, + absolute_row, + self.using_alternate_screen + ); + let (response, placement_result) = if self.using_alternate_screen { + if let Some(ref mut alt) = self.alternate_screen { + alt.image_storage.process_command( + cmd, + self.cursor_col, + absolute_row, + self.cell_width, + self.cell_height, + ) + } else { + // This should not happen if using_alternate_screen is true + (None, None) + } + } else { self.image_storage.process_command( cmd, self.cursor_col, absolute_row, self.cell_width, self.cell_height, - ); + ) + }; // Queue the response to send back to the application if let Some(resp) = response { self.response_queue.extend_from_slice(resp.as_bytes()); } - // Move cursor after image placement per Kitty protocol spec: - // "After placing an image on the screen the cursor must be moved to the - // right by the number of cols in the image placement rectangle and down - // by the number of rows in the image placement rectangle." - // However, if C=1 was specified, don't move the cursor. - if let Some(placement) = placement_result { - if !placement.suppress_cursor_move - && !placement.virtual_placement - { - // Move cursor to the right and down by the image dimensions - self.cursor_col += placement.cols; - let new_row = self.cursor_row + placement.rows; - if new_row >= self.rows { - // Need to scroll - let scroll_amount = new_row - self.rows + 1; - self.scroll_up(scroll_amount); - self.cursor_row = self.rows - 1; - } else { - self.cursor_row = new_row; + // Move cursor after image placement per Kitty protocol spec: + // "After placing an image on the screen the cursor must be moved to the + // right by the number of cols in the image placement rectangle and down + // by the number of rows in the image placement rectangle." + // However, if C=1 was specified, don't move the cursor. + if let Some(placement) = placement_result { + self.dirty = true; + if !placement.suppress_cursor_move + && !placement.virtual_placement + { + // Move cursor to the right and and down by the image dimensions + self.cursor_col += placement.cols; + let new_row = self.cursor_row + placement.rows; + if new_row >= self.rows { + // Need to scroll + let scroll_amount = new_row - self.rows + 1; + self.scroll_up(scroll_amount); + self.cursor_row = self.rows - 1; + } else { + self.cursor_row = new_row; + } + // If cursor is now beyond the right edge, it will be handled by the normal + // cursor movement logic (wrapping/scrolling) if applicable. + log::debug!( + "Cursor moved after image placement: col={}, row={} (moved {}x{} cells)", + self.cursor_col, self.cursor_row, placement.cols, placement.rows + ); + } } - // If cursor is now beyond the right edge, it will be handled by the normal - // cursor movement logic (wrapping/scrolling) if applicable. - log::debug!( - "Cursor moved after image placement: col={}, row={} (moved {}x{} cells)", - self.cursor_col, self.cursor_row, placement.cols, placement.rows - ); - } - } } } }