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