Skip to main content

par_term_render/cell_renderer/
mod.rs

1// ARC-009 TODO: When this file exceeds the 800-line limit, extract into sub-modules
2// under cell_renderer/:
3//
4//   glyph_ops.rs     — get_or_rasterize_glyph helper. Note: the glyph cache logic
5//                      was previously duplicated 3x but is now centralized in
6//                      atlas.rs (rasterize_glyph / get_or_rasterize_glyph /
7//                      resolve_glyph_with_fallback); the only remaining TODO here
8//                      is the QA-013 scratch-buffer extraction.
9//   font_fallback.rs — font fallback chain construction
10//
11// Tracking: Issue ARC-009 in AUDIT.md. See also QA-006 (glyph cache deduplication).
12
13use anyhow::{Context, Result};
14use std::collections::HashMap;
15use std::sync::Arc;
16use winit::window::Window;
17
18use crate::scrollbar::Scrollbar;
19use crate::wgpu_conversions::{PowerPreferenceWgpu, VsyncModeWgpu};
20use par_term_config::{SeparatorMark, color_u8_to_f32_a};
21use par_term_fonts::font_manager::FontManager;
22
23pub mod atlas;
24pub mod background;
25mod bg_instance_builder;
26pub mod block_chars;
27mod cursor;
28mod font;
29mod instance_buffers;
30mod layout;
31pub(crate) mod pane_render;
32pub mod pipeline;
33pub mod render;
34mod settings;
35pub mod surface;
36mod text_instance_builder;
37pub mod types;
38// Re-export public types for external use
39pub(crate) use pane_render::{PaneRenderViewParams, pane_instance_capacity};
40pub use types::{Cell, PaneViewport};
41// Re-export internal types for use within the cell_renderer module
42pub(crate) use types::{BackgroundInstance, GlyphInfo, RowCacheEntry, TextInstance};
43// Re-export instance buffer constants so mod.rs can reference them
44pub(crate) use instance_buffers::{CURSOR_OVERLAY_SLOTS, TEXT_INSTANCES_PER_CELL};
45// Re-export extracted sub-module types for use within this module
46pub(crate) use cursor::CursorState;
47pub(crate) use font::FontState;
48pub(crate) use layout::GridLayout;
49
50/// Physical DPI on macOS (points-based at 72 ppi).
51pub(crate) const MACOS_PLATFORM_DPI: f32 = 72.0;
52
53/// Physical DPI on non-macOS platforms (screen pixels at 96 ppi).
54pub(crate) const DEFAULT_PLATFORM_DPI: f32 = 96.0;
55
56/// Reference DPI used in the font-size conversion formula.
57/// Font sizes are specified in typographic points at 72 ppi.
58pub(crate) const FONT_REFERENCE_DPI: f32 = 72.0;
59
60/// Size (width and height) of the solid white pixel block uploaded to the glyph atlas.
61/// A 2×2 block provides better sampling behaviour than a single texel at borders.
62const SOLID_PIXEL_SIZE: u32 = 2;
63
64/// Pixel padding added around each glyph in the atlas to prevent bilinear bleed.
65pub(crate) const ATLAS_GLYPH_PADDING: u32 = 2;
66
67/// Maximum frame latency hint passed to the wgpu surface configuration.
68/// Controls how many frames may be queued ahead of the display; 2 balances
69/// throughput against input latency.
70const SURFACE_FRAME_LATENCY: u32 = 2;
71
72/// Default cursor guide line opacity.
73/// A very low value keeps the guide visible without overpowering text.
74const DEFAULT_GUIDE_OPACITY: f32 = 0.08;
75
76/// Default cursor shadow alpha component.
77const DEFAULT_SHADOW_ALPHA: f32 = 0.5;
78
79/// Default cursor shadow offset in pixels (x and y).
80const DEFAULT_SHADOW_OFFSET_PX: f32 = 2.0;
81
82/// Default cursor shadow blur radius in pixels.
83const DEFAULT_SHADOW_BLUR_PX: f32 = 3.0;
84
85/// GPU render pipelines and their associated bind group layouts.
86pub(crate) struct GpuPipelines {
87    pub(crate) bg_pipeline: wgpu::RenderPipeline,
88    pub(crate) text_pipeline: wgpu::RenderPipeline,
89    pub(crate) bg_image_pipeline: wgpu::RenderPipeline,
90    /// Full-screen flash pipeline used by `render_overlays` when `visual_bell_intensity > 0`.
91    pub(crate) visual_bell_pipeline: wgpu::RenderPipeline,
92    pub(crate) text_bind_group: wgpu::BindGroup,
93    #[allow(dead_code)] // GPU lifetime: must outlive bind groups created from this layout
94    pub(crate) text_bind_group_layout: wgpu::BindGroupLayout,
95    pub(crate) bg_image_bind_group: Option<wgpu::BindGroup>,
96    pub(crate) bg_image_bind_group_layout: wgpu::BindGroupLayout,
97    /// Bind group holding the visual bell uniform buffer; set in `render_overlays`.
98    pub(crate) visual_bell_bind_group: wgpu::BindGroup,
99    /// Pipeline that stamps alpha=1.0 over the entire surface (opaque window guard).
100    pub(crate) opaque_alpha_pipeline: wgpu::RenderPipeline,
101}
102
103/// GPU vertex, instance, and uniform buffers with capacity tracking.
104pub(crate) struct GpuBuffers {
105    pub(crate) vertex_buffer: wgpu::Buffer,
106    pub(crate) bg_instance_buffer: wgpu::Buffer,
107    pub(crate) text_instance_buffer: wgpu::Buffer,
108    pub(crate) bg_image_uniform_buffer: wgpu::Buffer,
109    /// Uniform buffer written each frame in `render_overlays` with position/color/intensity.
110    pub(crate) visual_bell_uniform_buffer: wgpu::Buffer,
111    /// Maximum capacity of the bg_instance_buffer (GPU buffer size)
112    pub(crate) max_bg_instances: usize,
113    /// Maximum capacity of the text_instance_buffer (GPU buffer size)
114    pub(crate) max_text_instances: usize,
115    /// Actual number of background instances written (used for draw calls)
116    pub(crate) actual_bg_instances: usize,
117    /// Actual number of text instances written (used for draw calls)
118    pub(crate) actual_text_instances: usize,
119    /// Next free `bg_instances` slot in the pane batch being built.
120    ///
121    /// ARC-004: panes suballocate the shared buffers instead of each restarting at
122    /// index 0, so every pane's instances stay resident and the whole frame can be
123    /// drawn from one command encoder. Reset by `begin_pane_batch`.
124    pub(crate) pane_bg_cursor: usize,
125    /// Next free `text_instances` slot in the pane batch being built.
126    /// See `pane_bg_cursor`.
127    pub(crate) pane_text_cursor: usize,
128    /// Set once after an instance-buffer over-run has been reported, so the error
129    /// is logged once per buffer allocation rather than once per frame.
130    pub(crate) overflow_reported: bool,
131}
132
133/// Glyph atlas texture, cache, and LRU eviction state.
134pub(crate) struct GlyphAtlas {
135    pub(crate) atlas_texture: wgpu::Texture,
136    #[allow(dead_code)] // GPU lifetime: must outlive text_bind_group which references this view
137    pub(crate) atlas_view: wgpu::TextureView,
138    pub(crate) glyph_cache: HashMap<u64, GlyphInfo>,
139    pub(crate) lru_head: Option<u64>,
140    pub(crate) lru_tail: Option<u64>,
141    pub(crate) atlas_next_x: u32,
142    pub(crate) atlas_next_y: u32,
143    pub(crate) atlas_row_height: u32,
144    /// Actual atlas size (may be smaller than preferred on devices with low texture limits)
145    pub(crate) atlas_size: u32,
146    /// Solid white pixel offset in atlas for geometric block rendering
147    pub(crate) solid_pixel_offset: (u32, u32),
148}
149
150/// Background image/solid-color texture state and per-pane cache.
151pub(crate) struct BackgroundImageState {
152    pub(crate) bg_image_texture: Option<wgpu::Texture>,
153    pub(crate) bg_image_mode: par_term_config::BackgroundImageMode,
154    pub(crate) bg_image_opacity: f32,
155    pub(crate) bg_image_width: u32,
156    pub(crate) bg_image_height: u32,
157    /// When true, current background is a solid color (not an image).
158    /// Solid colors should be rendered via clear color to respect window_opacity,
159    /// not via bg_image_pipeline which would cover the transparent background.
160    pub(crate) bg_is_solid_color: bool,
161    /// The solid background color [R, G, B] as floats (0.0-1.0).
162    /// Only used when bg_is_solid_color is true.
163    pub(crate) solid_bg_color: [f32; 3],
164    /// Cache of per-pane background textures keyed by image path
165    pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
166    /// Cache of per-pane uniform buffers and bind groups keyed by **pane index**.
167    /// Reused across frames via `queue.write_buffer()` to avoid per-frame GPU allocations.
168    ///
169    /// ARC-004: this was keyed by image path, but the uniform carries the pane's
170    /// position and size, so two panes sharing one background image aliased onto a
171    /// single buffer — previously masked by the `queue.submit` between panes, and
172    /// fatal once they are batched. The entry records its path so the bind group
173    /// can be rebuilt when a pane's image changes.
174    pub(crate) pane_bg_uniform_cache: HashMap<usize, background::PaneBgUniformEntry>,
175}
176
177/// Command separator line settings and visible marks.
178pub(crate) struct SeparatorConfig {
179    /// Whether to render separator lines between commands
180    pub(crate) enabled: bool,
181    /// Thickness of separator lines in pixels
182    pub(crate) thickness: f32,
183    /// Opacity of separator lines (0.0-1.0)
184    pub(crate) opacity: f32,
185    /// Whether to color separator lines by exit code
186    pub(crate) exit_color: bool,
187    /// Custom separator color [R, G, B] as floats (0.0-1.0)
188    pub(crate) color: [f32; 3],
189    /// Visible separator marks for current frame: (screen_row, exit_code, custom_color)
190    pub(crate) visible_marks: Vec<SeparatorMark>,
191}
192
193pub struct CellRenderer {
194    // Core wgpu state
195    pub(crate) device: Arc<wgpu::Device>,
196    pub(crate) queue: Arc<wgpu::Queue>,
197    pub(crate) surface: wgpu::Surface<'static>,
198    pub(crate) config: wgpu::SurfaceConfiguration,
199    /// Supported present modes for this surface (for vsync mode validation)
200    pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
201
202    // Sub-structs grouping related GPU and rendering state
203    pub(crate) pipelines: GpuPipelines,
204    pub(crate) buffers: GpuBuffers,
205    pub(crate) atlas: GlyphAtlas,
206    pub(crate) grid: GridLayout,
207    pub(crate) cursor: CursorState,
208    pub(crate) font: FontState,
209    pub(crate) bg_state: BackgroundImageState,
210    pub(crate) separator: SeparatorConfig,
211
212    /// Display scale factor (accessed directly from renderer module)
213    pub(crate) scale_factor: f32,
214
215    // Components
216    pub(crate) font_manager: FontManager,
217    pub(crate) scrollbar: Scrollbar,
218
219    // Dynamic state
220    pub(crate) cells: Vec<Cell>,
221    pub(crate) dirty_rows: Vec<bool>,
222    pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
223
224    // Rendering state
225    pub(crate) visual_bell_intensity: f32,
226    pub(crate) visual_bell_color: [f32; 3],
227    pub(crate) window_opacity: f32,
228    pub(crate) background_color: [f32; 4],
229    /// Whether the window is currently focused (for unfocused cursor style)
230    pub(crate) is_focused: bool,
231
232    // CPU-side instance buffers for incremental updates
233    pub(crate) bg_instances: Vec<BackgroundInstance>,
234    pub(crate) text_instances: Vec<TextInstance>,
235
236    // Scratch buffers reused across dirty-row iterations (avoids per-row Vec allocation)
237    pub(crate) scratch_row_bg: Vec<BackgroundInstance>,
238    pub(crate) scratch_row_text: Vec<TextInstance>,
239    /// Scratch buffer for a single row of cells, reused in `build_instance_buffers` to
240    /// avoid cloning `self.cells[start..end]` into a new Vec on every dirty row.
241    pub(crate) scratch_row_cells: Vec<Cell>,
242
243    /// Reusable swash ScaleContext — holds internal caches that must be preserved
244    /// across glyph rasterization calls. Allocating one per glyph throws away these
245    /// caches unnecessarily; keeping it here allows every rasterize_glyph call to
246    /// reuse the same warmed-up context.
247    pub(crate) scale_context: swash::scale::ScaleContext,
248
249    // Transparency mode
250    /// When true, only default background cells are transparent.
251    /// Non-default (colored) backgrounds remain opaque for readability.
252    pub(crate) transparency_affects_only_default_background: bool,
253    /// When true, text is always rendered at full opacity regardless of window transparency.
254    pub(crate) keep_text_opaque: bool,
255    /// Style for link underlines (solid or stipple)
256    pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
257
258    /// Gutter indicator marks for current frame: (screen_row, rgba_color)
259    pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
260}
261
262/// Configuration for [`CellRenderer::new`].
263///
264/// Bundles all font, grid, scrollbar, and background parameters so the
265/// constructor does not exceed the `clippy::too_many_arguments` threshold.
266pub struct CellRendererConfig<'a> {
267    /// Pre-built font manager.
268    ///
269    /// Passed in rather than constructed here because the caller already needs
270    /// one to derive `cols`/`rows`; building a second would enumerate every
271    /// system font again on each renderer rebuild (i.e. on every font-size change).
272    pub font_manager: FontManager,
273    pub font_size: f32,
274    pub cols: usize,
275    pub rows: usize,
276    pub window_padding: f32,
277    pub line_spacing: f32,
278    pub char_spacing: f32,
279    pub scrollbar_position: &'a str,
280    pub scrollbar_width: f32,
281    pub scrollbar_thumb_color: [f32; 4],
282    pub scrollbar_track_color: [f32; 4],
283    pub enable_text_shaping: bool,
284    pub enable_ligatures: bool,
285    pub enable_kerning: bool,
286    pub font_antialias: bool,
287    pub font_hinting: bool,
288    pub font_thin_strokes: par_term_config::ThinStrokesMode,
289    pub minimum_contrast: f32,
290    pub vsync_mode: par_term_config::VsyncMode,
291    pub power_preference: par_term_config::PowerPreference,
292    pub window_opacity: f32,
293    pub background_color: [u8; 3],
294    pub background_image_path: Option<&'a str>,
295    pub background_image_mode: par_term_config::BackgroundImageMode,
296    pub background_image_opacity: f32,
297}
298
299impl CellRenderer {
300    pub async fn new(window: Arc<Window>, config: CellRendererConfig<'_>) -> Result<Self> {
301        let CellRendererConfig {
302            font_manager,
303            font_size,
304            cols,
305            rows,
306            window_padding,
307            line_spacing,
308            char_spacing,
309            scrollbar_position,
310            scrollbar_width,
311            scrollbar_thumb_color,
312            scrollbar_track_color,
313            enable_text_shaping,
314            enable_ligatures,
315            enable_kerning,
316            font_antialias,
317            font_hinting,
318            font_thin_strokes,
319            minimum_contrast,
320            vsync_mode,
321            power_preference,
322            window_opacity,
323            background_color,
324            background_image_path,
325            background_image_mode,
326            background_image_opacity,
327        } = config;
328        // Platform-specific backend selection for better VM compatibility
329        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
330        // macOS: Use Metal (native)
331        // Linux: Try Vulkan first, fall back to GL for VM compatibility
332        // Platform-specific backend selection for better VM compatibility
333        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
334        // macOS: Use Metal (native)
335        // Linux: Try Vulkan first, fall back to GL for VM compatibility
336        #[cfg(target_os = "windows")]
337        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
338            backends: wgpu::Backends::DX12,
339            ..wgpu::InstanceDescriptor::new_without_display_handle()
340        });
341        #[cfg(target_os = "macos")]
342        let instance = wgpu::Instance::default();
343        #[cfg(target_os = "linux")]
344        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
345            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
346            ..wgpu::InstanceDescriptor::new_without_display_handle()
347        });
348        let surface = instance.create_surface(window.clone())?;
349        let adapter = instance
350            .request_adapter(&wgpu::RequestAdapterOptions {
351                power_preference: power_preference.to_wgpu(),
352                compatible_surface: Some(&surface),
353                force_fallback_adapter: false,
354                apply_limit_buckets: false,
355            })
356            .await
357            .context("Failed to find wgpu adapter")?;
358
359        let (device, queue) = adapter
360            .request_device(&wgpu::DeviceDescriptor {
361                label: Some("device"),
362                required_features: wgpu::Features::empty(),
363                required_limits: surface::texture_limits(adapter.limits().max_texture_dimension_2d),
364                memory_hints: wgpu::MemoryHints::default(),
365                ..Default::default()
366            })
367            .await?;
368        surface::install_nonfatal_error_handler(&device);
369
370        let device = Arc::new(device);
371        let queue = Arc::new(queue);
372
373        let size = window.inner_size();
374        let surface_caps = surface.get_capabilities(&adapter);
375        let surface_format = surface_caps
376            .formats
377            .iter()
378            .copied()
379            .find(|f| !f.is_srgb())
380            .or_else(|| surface_caps.formats.first().copied())
381            .context("Surface reports no supported texture formats")?;
382
383        // Store supported present modes for runtime validation
384        let supported_present_modes = surface_caps.present_modes.clone();
385
386        // Select present mode with fallback if requested mode isn't supported
387        let requested_mode = vsync_mode.to_present_mode();
388        let present_mode = if supported_present_modes.contains(&requested_mode) {
389            requested_mode
390        } else {
391            // Fall back to Fifo (always supported) or first available
392            log::warn!(
393                "Requested present mode {:?} not supported (available: {:?}), falling back",
394                requested_mode,
395                supported_present_modes
396            );
397            if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
398                wgpu::PresentMode::Fifo
399            } else {
400                supported_present_modes
401                    .first()
402                    .copied()
403                    .context("Surface reports no supported present modes")?
404            }
405        };
406
407        // Select alpha mode for window transparency
408        // Prefer PreMultiplied (best for compositing) > PostMultiplied > Auto > first available
409        let alpha_mode = if surface_caps
410            .alpha_modes
411            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
412        {
413            wgpu::CompositeAlphaMode::PreMultiplied
414        } else if surface_caps
415            .alpha_modes
416            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
417        {
418            wgpu::CompositeAlphaMode::PostMultiplied
419        } else if surface_caps
420            .alpha_modes
421            .contains(&wgpu::CompositeAlphaMode::Auto)
422        {
423            wgpu::CompositeAlphaMode::Auto
424        } else {
425            surface_caps
426                .alpha_modes
427                .first()
428                .copied()
429                .context("Surface reports no supported alpha modes")?
430        };
431        log::info!(
432            "Selected alpha mode: {:?} (available: {:?})",
433            alpha_mode,
434            surface_caps.alpha_modes
435        );
436
437        let (surface_width, surface_height) = surface::clamp_surface_extent(
438            size.width,
439            size.height,
440            device.limits().max_texture_dimension_2d,
441        );
442        let config = wgpu::SurfaceConfiguration {
443            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
444            format: surface_format,
445            color_space: wgpu::SurfaceColorSpace::Auto,
446            width: surface_width,
447            height: surface_height,
448            present_mode,
449            alpha_mode,
450            view_formats: vec![],
451            desired_maximum_frame_latency: SURFACE_FRAME_LATENCY,
452        };
453        log::info!(
454            "Surface configured: {}x{} {:?} present={:?} alpha={:?} color_space={:?} frame_latency={}",
455            surface_width,
456            surface_height,
457            surface_format,
458            present_mode,
459            alpha_mode,
460            config.color_space,
461            SURFACE_FRAME_LATENCY
462        );
463        surface.configure(&device, &config);
464
465        let scale_factor = window.scale_factor() as f32;
466
467        let platform_dpi = if cfg!(target_os = "macos") {
468            MACOS_PLATFORM_DPI
469        } else {
470            DEFAULT_PLATFORM_DPI
471        };
472
473        let base_font_pixels = font_size * platform_dpi / FONT_REFERENCE_DPI;
474        let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
475
476        // Extract font metrics
477        let (font_ascent, font_descent, font_leading, char_advance) = {
478            let primary_font = font_manager
479                .get_font(0)
480                .expect("Primary font at index 0 must exist after FontManager initialization");
481            let metrics = primary_font.metrics(&[]);
482            let scale = font_size_pixels / metrics.units_per_em as f32;
483            let glyph_id = primary_font.charmap().map('m');
484            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
485            (
486                metrics.ascent * scale,
487                metrics.descent * scale,
488                metrics.leading * scale,
489                advance,
490            )
491        };
492
493        let natural_line_height = font_ascent + font_descent + font_leading;
494        // Round to integer pixels so every cell has the same width in device pixels.
495        // Without rounding, per-column scale_x alternates (e.g. 7/7.8 vs 8/7.8),
496        // causing glyphs to sample the atlas at slightly different rates and appear
497        // at different perceived brightnesses.
498        let cell_height = (natural_line_height * line_spacing).max(1.0).round();
499        let cell_width = (char_advance * char_spacing).max(1.0).round();
500
501        let scrollbar = Scrollbar::new(
502            Arc::clone(&device),
503            surface_format,
504            scrollbar_width,
505            scrollbar_position,
506            scrollbar_thumb_color,
507            scrollbar_track_color,
508        );
509
510        // Create pipelines using the pipeline module
511        let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
512
513        let (atlas_texture, atlas_view, atlas_sampler, atlas_size) =
514            pipeline::create_atlas(&device);
515        let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
516        let text_bind_group = pipeline::create_text_bind_group(
517            &device,
518            &text_bind_group_layout,
519            &atlas_view,
520            &atlas_sampler,
521        );
522        let text_pipeline =
523            pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
524
525        let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
526        let bg_image_pipeline = pipeline::create_bg_image_pipeline(
527            &device,
528            surface_format,
529            &bg_image_bind_group_layout,
530        );
531        let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
532
533        let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
534            pipeline::create_visual_bell_pipeline(&device, surface_format);
535
536        let opaque_alpha_pipeline = pipeline::create_opaque_alpha_pipeline(&device, surface_format);
537
538        let vertex_buffer = pipeline::create_vertex_buffer(&device);
539
540        // Instance buffers
541        let max_bg_instances = render::SingleGridLayout::new(cols, rows).bg_instances();
542        let max_text_instances = cols * rows * TEXT_INSTANCES_PER_CELL;
543        let (bg_instance_buffer, text_instance_buffer) =
544            pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
545
546        let mut renderer = Self {
547            device,
548            queue,
549            surface,
550            config,
551            supported_present_modes,
552            pipelines: GpuPipelines {
553                bg_pipeline,
554                text_pipeline,
555                bg_image_pipeline,
556                visual_bell_pipeline,
557                text_bind_group,
558                text_bind_group_layout,
559                bg_image_bind_group: None,
560                bg_image_bind_group_layout,
561                visual_bell_bind_group,
562                opaque_alpha_pipeline,
563            },
564            buffers: GpuBuffers {
565                vertex_buffer,
566                bg_instance_buffer,
567                text_instance_buffer,
568                bg_image_uniform_buffer,
569                visual_bell_uniform_buffer,
570                max_bg_instances,
571                max_text_instances,
572                actual_bg_instances: 0,
573                actual_text_instances: 0,
574                pane_bg_cursor: 0,
575                pane_text_cursor: 0,
576                overflow_reported: false,
577            },
578            atlas: GlyphAtlas {
579                atlas_texture,
580                atlas_view,
581                glyph_cache: HashMap::new(),
582                lru_head: None,
583                lru_tail: None,
584                atlas_next_x: 0,
585                atlas_next_y: 0,
586                atlas_row_height: 0,
587                atlas_size,
588                solid_pixel_offset: (0, 0),
589            },
590            grid: GridLayout {
591                cols,
592                rows,
593                cell_width,
594                cell_height,
595                window_padding,
596                content_offset_y: 0.0,
597                content_offset_x: 0.0,
598                content_inset_bottom: 0.0,
599                content_inset_right: 0.0,
600                egui_bottom_inset: 0.0,
601                egui_right_inset: 0.0,
602            },
603            cursor: CursorState {
604                pos: (0, 0),
605                opacity: 0.0,
606                style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
607                color: [1.0, 1.0, 1.0],
608                text_color: None,
609                hidden_for_shader: false,
610                guide_enabled: false,
611                guide_color: [1.0, 1.0, 1.0, DEFAULT_GUIDE_OPACITY],
612                shadow_enabled: false,
613                shadow_color: [0.0, 0.0, 0.0, DEFAULT_SHADOW_ALPHA],
614                shadow_offset: [DEFAULT_SHADOW_OFFSET_PX, DEFAULT_SHADOW_OFFSET_PX],
615                shadow_blur: DEFAULT_SHADOW_BLUR_PX,
616                boost: 0.0,
617                boost_color: [1.0, 1.0, 1.0],
618                unfocused_style: par_term_config::UnfocusedCursorStyle::default(),
619            },
620            font: FontState {
621                base_font_size: font_size,
622                line_spacing,
623                char_spacing,
624                font_ascent,
625                font_descent,
626                font_leading,
627                font_size_pixels,
628                char_advance,
629                enable_text_shaping,
630                enable_ligatures,
631                enable_kerning,
632                font_antialias,
633                font_hinting,
634                font_thin_strokes,
635                minimum_contrast: minimum_contrast.clamp(0.0, 1.0),
636            },
637            bg_state: BackgroundImageState {
638                bg_image_texture: None,
639                bg_image_mode: background_image_mode,
640                bg_image_opacity: background_image_opacity,
641                bg_image_width: 0,
642                bg_image_height: 0,
643                bg_is_solid_color: false,
644                solid_bg_color: [0.0, 0.0, 0.0],
645                pane_bg_cache: HashMap::new(),
646                pane_bg_uniform_cache: HashMap::new(),
647            },
648            separator: SeparatorConfig {
649                enabled: false,
650                thickness: 1.0,
651                opacity: 0.4,
652                exit_color: true,
653                color: [0.5, 0.5, 0.5],
654                visible_marks: Vec::new(),
655            },
656            scale_factor,
657            font_manager,
658            scrollbar,
659            cells: vec![Cell::default(); cols * rows],
660            dirty_rows: vec![true; rows],
661            row_cache: (0..rows).map(|_| None).collect(),
662            is_focused: true,
663            visual_bell_intensity: 0.0,
664            visual_bell_color: [1.0, 1.0, 1.0], // White flash
665            window_opacity,
666            background_color: color_u8_to_f32_a(background_color, 1.0),
667            bg_instances: vec![
668                BackgroundInstance {
669                    position: [0.0, 0.0],
670                    size: [0.0, 0.0],
671                    color: [0.0, 0.0, 0.0, 0.0],
672                };
673                max_bg_instances
674            ],
675            text_instances: vec![
676                TextInstance {
677                    position: [0.0, 0.0],
678                    size: [0.0, 0.0],
679                    tex_offset: [0.0, 0.0],
680                    tex_size: [0.0, 0.0],
681                    color: [0.0, 0.0, 0.0, 0.0],
682                    is_colored: 0,
683                };
684                max_text_instances
685            ],
686            transparency_affects_only_default_background: false,
687            keep_text_opaque: true,
688            link_underline_style: par_term_config::LinkUnderlineStyle::default(),
689            gutter_indicators: Vec::new(),
690            scratch_row_bg: Vec::with_capacity(cols),
691            scratch_row_text: Vec::with_capacity(cols * 2),
692            scratch_row_cells: Vec::with_capacity(cols),
693            scale_context: swash::scale::ScaleContext::new(),
694        };
695
696        // Upload a solid white 2x2 pixel block to the atlas for geometric block rendering
697        renderer.upload_solid_pixel();
698
699        log::info!(
700            "CellRenderer::new: background_image_path={:?}",
701            background_image_path
702        );
703        if let Some(path) = background_image_path {
704            // Handle missing background image gracefully - don't crash, just log and continue
705            if let Err(e) = renderer.load_background_image(path) {
706                log::warn!(
707                    "Could not load background image '{}': {} - continuing without background image",
708                    path,
709                    e
710                );
711            }
712        }
713
714        Ok(renderer)
715    }
716
717    /// Upload a solid white pixel to the atlas for use in geometric block rendering
718    pub(crate) fn upload_solid_pixel(&mut self) {
719        let size = SOLID_PIXEL_SIZE;
720        let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
721
722        self.queue.write_texture(
723            wgpu::TexelCopyTextureInfo {
724                texture: &self.atlas.atlas_texture,
725                mip_level: 0,
726                origin: wgpu::Origin3d {
727                    x: self.atlas.atlas_next_x,
728                    y: self.atlas.atlas_next_y,
729                    z: 0,
730                },
731                aspect: wgpu::TextureAspect::All,
732            },
733            &white_pixels,
734            wgpu::TexelCopyBufferLayout {
735                offset: 0,
736                bytes_per_row: Some(4 * size),
737                rows_per_image: Some(size),
738            },
739            wgpu::Extent3d {
740                width: size,
741                height: size,
742                depth_or_array_layers: 1,
743            },
744        );
745
746        self.atlas.solid_pixel_offset = (self.atlas.atlas_next_x, self.atlas.atlas_next_y);
747        self.atlas.atlas_next_x += size + ATLAS_GLYPH_PADDING;
748        self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(size);
749    }
750
751    pub fn device(&self) -> &wgpu::Device {
752        &self.device
753    }
754    pub fn queue(&self) -> &wgpu::Queue {
755        &self.queue
756    }
757    pub fn surface_format(&self) -> wgpu::TextureFormat {
758        self.config.format
759    }
760    pub fn keep_text_opaque(&self) -> bool {
761        self.keep_text_opaque
762    }
763
764    /// Update cells. Returns `true` if any incoming cell actually changed.
765    ///
766    /// The return value is the point: `Renderer::update_cells` turns it into
767    /// `Renderer::dirty`, which gates whether a frame renders. `new_cells` is the
768    /// focused pane's buffer, whose stride is not `self.grid.cols`; see
769    /// `build_instance_buffers` for why that is now harmless and why the trailing
770    /// partial row must still be compared.
771    pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
772        let n = new_cells.len().min(self.cells.len());
773        let mut changed = false;
774        for row in 0..self.grid.rows {
775            let start = row * self.grid.cols;
776            if start >= n {
777                break;
778            }
779            let end = ((row + 1) * self.grid.cols).min(n);
780            let row_slice = &new_cells[start..end];
781            if row_slice != &self.cells[start..end] {
782                self.cells[start..end].clone_from_slice(row_slice);
783                self.dirty_rows[row] = true;
784                changed = true;
785            }
786        }
787        changed
788    }
789
790    /// Clear all cells and mark all rows as dirty.
791    pub fn clear_all_cells(&mut self) {
792        for cell in &mut self.cells {
793            *cell = Cell::default();
794        }
795        self.dirty_rows.fill(true);
796    }
797
798    pub fn update_graphics(
799        &mut self,
800        _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
801        _scroll_offset: usize,
802        _scrollback_len: usize,
803        _visible_lines: usize,
804    ) -> Result<()> {
805        Ok(())
806    }
807}