1use 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;
35mod surface;
36mod text_instance_builder;
37pub mod types;
38pub(crate) use pane_render::PaneRenderViewParams;
40pub use types::{Cell, PaneViewport};
41pub(crate) use types::{BackgroundInstance, GlyphInfo, RowCacheEntry, TextInstance};
43pub(crate) use instance_buffers::{CURSOR_OVERLAY_SLOTS, TEXT_INSTANCES_PER_CELL};
45pub(crate) use cursor::CursorState;
47pub(crate) use font::FontState;
48pub(crate) use layout::GridLayout;
49
50pub(crate) const MACOS_PLATFORM_DPI: f32 = 72.0;
52
53pub(crate) const DEFAULT_PLATFORM_DPI: f32 = 96.0;
55
56pub(crate) const FONT_REFERENCE_DPI: f32 = 72.0;
59
60const SOLID_PIXEL_SIZE: u32 = 2;
63
64pub(crate) const ATLAS_GLYPH_PADDING: u32 = 2;
66
67const SURFACE_FRAME_LATENCY: u32 = 2;
71
72const DEFAULT_GUIDE_OPACITY: f32 = 0.08;
75
76const DEFAULT_SHADOW_ALPHA: f32 = 0.5;
78
79const DEFAULT_SHADOW_OFFSET_PX: f32 = 2.0;
81
82const DEFAULT_SHADOW_BLUR_PX: f32 = 3.0;
84
85pub(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 pub(crate) visual_bell_pipeline: wgpu::RenderPipeline,
92 pub(crate) text_bind_group: wgpu::BindGroup,
93 #[allow(dead_code)] 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 pub(crate) visual_bell_bind_group: wgpu::BindGroup,
99 pub(crate) opaque_alpha_pipeline: wgpu::RenderPipeline,
101}
102
103pub(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 pub(crate) visual_bell_uniform_buffer: wgpu::Buffer,
111 pub(crate) max_bg_instances: usize,
113 pub(crate) max_text_instances: usize,
115 pub(crate) actual_bg_instances: usize,
117 pub(crate) actual_text_instances: usize,
119}
120
121pub(crate) struct GlyphAtlas {
123 pub(crate) atlas_texture: wgpu::Texture,
124 #[allow(dead_code)] pub(crate) atlas_view: wgpu::TextureView,
126 pub(crate) glyph_cache: HashMap<u64, GlyphInfo>,
127 pub(crate) lru_head: Option<u64>,
128 pub(crate) lru_tail: Option<u64>,
129 pub(crate) atlas_next_x: u32,
130 pub(crate) atlas_next_y: u32,
131 pub(crate) atlas_row_height: u32,
132 pub(crate) atlas_size: u32,
134 pub(crate) solid_pixel_offset: (u32, u32),
136}
137
138pub(crate) struct BackgroundImageState {
140 pub(crate) bg_image_texture: Option<wgpu::Texture>,
141 pub(crate) bg_image_mode: par_term_config::BackgroundImageMode,
142 pub(crate) bg_image_opacity: f32,
143 pub(crate) bg_image_width: u32,
144 pub(crate) bg_image_height: u32,
145 pub(crate) bg_is_solid_color: bool,
149 pub(crate) solid_bg_color: [f32; 3],
152 pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
154 pub(crate) pane_bg_uniform_cache: HashMap<String, background::PaneBgUniformEntry>,
157}
158
159pub(crate) struct SeparatorConfig {
161 pub(crate) enabled: bool,
163 pub(crate) thickness: f32,
165 pub(crate) opacity: f32,
167 pub(crate) exit_color: bool,
169 pub(crate) color: [f32; 3],
171 pub(crate) visible_marks: Vec<SeparatorMark>,
173}
174
175pub struct CellRenderer {
176 pub(crate) device: Arc<wgpu::Device>,
178 pub(crate) queue: Arc<wgpu::Queue>,
179 pub(crate) surface: wgpu::Surface<'static>,
180 pub(crate) config: wgpu::SurfaceConfiguration,
181 pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
183
184 pub(crate) pipelines: GpuPipelines,
186 pub(crate) buffers: GpuBuffers,
187 pub(crate) atlas: GlyphAtlas,
188 pub(crate) grid: GridLayout,
189 pub(crate) cursor: CursorState,
190 pub(crate) font: FontState,
191 pub(crate) bg_state: BackgroundImageState,
192 pub(crate) separator: SeparatorConfig,
193
194 pub(crate) scale_factor: f32,
196
197 pub(crate) font_manager: FontManager,
199 pub(crate) scrollbar: Scrollbar,
200
201 pub(crate) cells: Vec<Cell>,
203 pub(crate) dirty_rows: Vec<bool>,
204 pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
205
206 pub(crate) visual_bell_intensity: f32,
208 pub(crate) visual_bell_color: [f32; 3],
209 pub(crate) window_opacity: f32,
210 pub(crate) background_color: [f32; 4],
211 pub(crate) is_focused: bool,
213
214 pub(crate) bg_instances: Vec<BackgroundInstance>,
216 pub(crate) text_instances: Vec<TextInstance>,
217
218 pub(crate) scratch_row_bg: Vec<BackgroundInstance>,
220 pub(crate) scratch_row_text: Vec<TextInstance>,
221 pub(crate) scratch_row_cells: Vec<Cell>,
224
225 pub(crate) scale_context: swash::scale::ScaleContext,
230
231 pub(crate) transparency_affects_only_default_background: bool,
235 pub(crate) keep_text_opaque: bool,
237 pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
239
240 pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
242}
243
244pub struct CellRendererConfig<'a> {
249 pub font_family: Option<&'a str>,
250 pub font_family_bold: Option<&'a str>,
251 pub font_family_italic: Option<&'a str>,
252 pub font_family_bold_italic: Option<&'a str>,
253 pub font_ranges: &'a [par_term_config::FontRange],
254 pub font_size: f32,
255 pub cols: usize,
256 pub rows: usize,
257 pub window_padding: f32,
258 pub line_spacing: f32,
259 pub char_spacing: f32,
260 pub scrollbar_position: &'a str,
261 pub scrollbar_width: f32,
262 pub scrollbar_thumb_color: [f32; 4],
263 pub scrollbar_track_color: [f32; 4],
264 pub enable_text_shaping: bool,
265 pub enable_ligatures: bool,
266 pub enable_kerning: bool,
267 pub font_antialias: bool,
268 pub font_hinting: bool,
269 pub font_thin_strokes: par_term_config::ThinStrokesMode,
270 pub minimum_contrast: f32,
271 pub vsync_mode: par_term_config::VsyncMode,
272 pub power_preference: par_term_config::PowerPreference,
273 pub window_opacity: f32,
274 pub background_color: [u8; 3],
275 pub background_image_path: Option<&'a str>,
276 pub background_image_mode: par_term_config::BackgroundImageMode,
277 pub background_image_opacity: f32,
278}
279
280impl CellRenderer {
281 pub async fn new(window: Arc<Window>, config: CellRendererConfig<'_>) -> Result<Self> {
282 let CellRendererConfig {
283 font_family,
284 font_family_bold,
285 font_family_italic,
286 font_family_bold_italic,
287 font_ranges,
288 font_size,
289 cols,
290 rows,
291 window_padding,
292 line_spacing,
293 char_spacing,
294 scrollbar_position,
295 scrollbar_width,
296 scrollbar_thumb_color,
297 scrollbar_track_color,
298 enable_text_shaping,
299 enable_ligatures,
300 enable_kerning,
301 font_antialias,
302 font_hinting,
303 font_thin_strokes,
304 minimum_contrast,
305 vsync_mode,
306 power_preference,
307 window_opacity,
308 background_color,
309 background_image_path,
310 background_image_mode,
311 background_image_opacity,
312 } = config;
313 #[cfg(target_os = "windows")]
322 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
323 backends: wgpu::Backends::DX12,
324 ..wgpu::InstanceDescriptor::new_without_display_handle()
325 });
326 #[cfg(target_os = "macos")]
327 let instance = wgpu::Instance::default();
328 #[cfg(target_os = "linux")]
329 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
330 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
331 ..wgpu::InstanceDescriptor::new_without_display_handle()
332 });
333 let surface = instance.create_surface(window.clone())?;
334 let adapter = instance
335 .request_adapter(&wgpu::RequestAdapterOptions {
336 power_preference: power_preference.to_wgpu(),
337 compatible_surface: Some(&surface),
338 force_fallback_adapter: false,
339 })
340 .await
341 .context("Failed to find wgpu adapter")?;
342
343 let (device, queue) = adapter
344 .request_device(&wgpu::DeviceDescriptor {
345 label: Some("device"),
346 required_features: wgpu::Features::empty(),
347 required_limits: wgpu::Limits::default(),
348 memory_hints: wgpu::MemoryHints::default(),
349 ..Default::default()
350 })
351 .await?;
352
353 let device = Arc::new(device);
354 let queue = Arc::new(queue);
355
356 let size = window.inner_size();
357 let surface_caps = surface.get_capabilities(&adapter);
358 let surface_format = surface_caps
359 .formats
360 .iter()
361 .copied()
362 .find(|f| !f.is_srgb())
363 .unwrap_or(surface_caps.formats[0]);
364
365 let supported_present_modes = surface_caps.present_modes.clone();
367
368 let requested_mode = vsync_mode.to_present_mode();
370 let present_mode = if supported_present_modes.contains(&requested_mode) {
371 requested_mode
372 } else {
373 log::warn!(
375 "Requested present mode {:?} not supported (available: {:?}), falling back",
376 requested_mode,
377 supported_present_modes
378 );
379 if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
380 wgpu::PresentMode::Fifo
381 } else {
382 supported_present_modes[0]
383 }
384 };
385
386 let alpha_mode = if surface_caps
389 .alpha_modes
390 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
391 {
392 wgpu::CompositeAlphaMode::PreMultiplied
393 } else if surface_caps
394 .alpha_modes
395 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
396 {
397 wgpu::CompositeAlphaMode::PostMultiplied
398 } else if surface_caps
399 .alpha_modes
400 .contains(&wgpu::CompositeAlphaMode::Auto)
401 {
402 wgpu::CompositeAlphaMode::Auto
403 } else {
404 surface_caps.alpha_modes[0]
405 };
406 log::info!(
407 "Selected alpha mode: {:?} (available: {:?})",
408 alpha_mode,
409 surface_caps.alpha_modes
410 );
411
412 let config = wgpu::SurfaceConfiguration {
413 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
414 format: surface_format,
415 width: size.width.max(1),
416 height: size.height.max(1),
417 present_mode,
418 alpha_mode,
419 view_formats: vec![],
420 desired_maximum_frame_latency: SURFACE_FRAME_LATENCY,
421 };
422 surface.configure(&device, &config);
423
424 let scale_factor = window.scale_factor() as f32;
425
426 let platform_dpi = if cfg!(target_os = "macos") {
427 MACOS_PLATFORM_DPI
428 } else {
429 DEFAULT_PLATFORM_DPI
430 };
431
432 let base_font_pixels = font_size * platform_dpi / FONT_REFERENCE_DPI;
433 let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
434
435 let font_manager = FontManager::new(
436 font_family,
437 font_family_bold,
438 font_family_italic,
439 font_family_bold_italic,
440 font_ranges,
441 )?;
442
443 let (font_ascent, font_descent, font_leading, char_advance) = {
445 let primary_font = font_manager
446 .get_font(0)
447 .expect("Primary font at index 0 must exist after FontManager initialization");
448 let metrics = primary_font.metrics(&[]);
449 let scale = font_size_pixels / metrics.units_per_em as f32;
450 let glyph_id = primary_font.charmap().map('m');
451 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
452 (
453 metrics.ascent * scale,
454 metrics.descent * scale,
455 metrics.leading * scale,
456 advance,
457 )
458 };
459
460 let natural_line_height = font_ascent + font_descent + font_leading;
461 let cell_height = (natural_line_height * line_spacing).max(1.0).round();
466 let cell_width = (char_advance * char_spacing).max(1.0).round();
467
468 let scrollbar = Scrollbar::new(
469 Arc::clone(&device),
470 surface_format,
471 scrollbar_width,
472 scrollbar_position,
473 scrollbar_thumb_color,
474 scrollbar_track_color,
475 );
476
477 let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
479
480 let (atlas_texture, atlas_view, atlas_sampler, atlas_size) =
481 pipeline::create_atlas(&device);
482 let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
483 let text_bind_group = pipeline::create_text_bind_group(
484 &device,
485 &text_bind_group_layout,
486 &atlas_view,
487 &atlas_sampler,
488 );
489 let text_pipeline =
490 pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
491
492 let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
493 let bg_image_pipeline = pipeline::create_bg_image_pipeline(
494 &device,
495 surface_format,
496 &bg_image_bind_group_layout,
497 );
498 let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
499
500 let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
501 pipeline::create_visual_bell_pipeline(&device, surface_format);
502
503 let opaque_alpha_pipeline = pipeline::create_opaque_alpha_pipeline(&device, surface_format);
504
505 let vertex_buffer = pipeline::create_vertex_buffer(&device);
506
507 let max_bg_instances = cols * rows + CURSOR_OVERLAY_SLOTS + rows + rows;
510 let max_text_instances = cols * rows * TEXT_INSTANCES_PER_CELL;
511 let (bg_instance_buffer, text_instance_buffer) =
512 pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
513
514 let mut renderer = Self {
515 device,
516 queue,
517 surface,
518 config,
519 supported_present_modes,
520 pipelines: GpuPipelines {
521 bg_pipeline,
522 text_pipeline,
523 bg_image_pipeline,
524 visual_bell_pipeline,
525 text_bind_group,
526 text_bind_group_layout,
527 bg_image_bind_group: None,
528 bg_image_bind_group_layout,
529 visual_bell_bind_group,
530 opaque_alpha_pipeline,
531 },
532 buffers: GpuBuffers {
533 vertex_buffer,
534 bg_instance_buffer,
535 text_instance_buffer,
536 bg_image_uniform_buffer,
537 visual_bell_uniform_buffer,
538 max_bg_instances,
539 max_text_instances,
540 actual_bg_instances: 0,
541 actual_text_instances: 0,
542 },
543 atlas: GlyphAtlas {
544 atlas_texture,
545 atlas_view,
546 glyph_cache: HashMap::new(),
547 lru_head: None,
548 lru_tail: None,
549 atlas_next_x: 0,
550 atlas_next_y: 0,
551 atlas_row_height: 0,
552 atlas_size,
553 solid_pixel_offset: (0, 0),
554 },
555 grid: GridLayout {
556 cols,
557 rows,
558 cell_width,
559 cell_height,
560 window_padding,
561 content_offset_y: 0.0,
562 content_offset_x: 0.0,
563 content_inset_bottom: 0.0,
564 content_inset_right: 0.0,
565 egui_bottom_inset: 0.0,
566 egui_right_inset: 0.0,
567 },
568 cursor: CursorState {
569 pos: (0, 0),
570 opacity: 0.0,
571 style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
572 color: [1.0, 1.0, 1.0],
573 text_color: None,
574 hidden_for_shader: false,
575 guide_enabled: false,
576 guide_color: [1.0, 1.0, 1.0, DEFAULT_GUIDE_OPACITY],
577 shadow_enabled: false,
578 shadow_color: [0.0, 0.0, 0.0, DEFAULT_SHADOW_ALPHA],
579 shadow_offset: [DEFAULT_SHADOW_OFFSET_PX, DEFAULT_SHADOW_OFFSET_PX],
580 shadow_blur: DEFAULT_SHADOW_BLUR_PX,
581 boost: 0.0,
582 boost_color: [1.0, 1.0, 1.0],
583 unfocused_style: par_term_config::UnfocusedCursorStyle::default(),
584 },
585 font: FontState {
586 base_font_size: font_size,
587 line_spacing,
588 char_spacing,
589 font_ascent,
590 font_descent,
591 font_leading,
592 font_size_pixels,
593 char_advance,
594 enable_text_shaping,
595 enable_ligatures,
596 enable_kerning,
597 font_antialias,
598 font_hinting,
599 font_thin_strokes,
600 minimum_contrast: minimum_contrast.clamp(0.0, 1.0),
601 },
602 bg_state: BackgroundImageState {
603 bg_image_texture: None,
604 bg_image_mode: background_image_mode,
605 bg_image_opacity: background_image_opacity,
606 bg_image_width: 0,
607 bg_image_height: 0,
608 bg_is_solid_color: false,
609 solid_bg_color: [0.0, 0.0, 0.0],
610 pane_bg_cache: HashMap::new(),
611 pane_bg_uniform_cache: HashMap::new(),
612 },
613 separator: SeparatorConfig {
614 enabled: false,
615 thickness: 1.0,
616 opacity: 0.4,
617 exit_color: true,
618 color: [0.5, 0.5, 0.5],
619 visible_marks: Vec::new(),
620 },
621 scale_factor,
622 font_manager,
623 scrollbar,
624 cells: vec![Cell::default(); cols * rows],
625 dirty_rows: vec![true; rows],
626 row_cache: (0..rows).map(|_| None).collect(),
627 is_focused: true,
628 visual_bell_intensity: 0.0,
629 visual_bell_color: [1.0, 1.0, 1.0], window_opacity,
631 background_color: color_u8_to_f32_a(background_color, 1.0),
632 bg_instances: vec![
633 BackgroundInstance {
634 position: [0.0, 0.0],
635 size: [0.0, 0.0],
636 color: [0.0, 0.0, 0.0, 0.0],
637 };
638 max_bg_instances
639 ],
640 text_instances: vec![
641 TextInstance {
642 position: [0.0, 0.0],
643 size: [0.0, 0.0],
644 tex_offset: [0.0, 0.0],
645 tex_size: [0.0, 0.0],
646 color: [0.0, 0.0, 0.0, 0.0],
647 is_colored: 0,
648 };
649 max_text_instances
650 ],
651 transparency_affects_only_default_background: false,
652 keep_text_opaque: true,
653 link_underline_style: par_term_config::LinkUnderlineStyle::default(),
654 gutter_indicators: Vec::new(),
655 scratch_row_bg: Vec::with_capacity(cols),
656 scratch_row_text: Vec::with_capacity(cols * 2),
657 scratch_row_cells: Vec::with_capacity(cols),
658 scale_context: swash::scale::ScaleContext::new(),
659 };
660
661 renderer.upload_solid_pixel();
663
664 log::info!(
665 "CellRenderer::new: background_image_path={:?}",
666 background_image_path
667 );
668 if let Some(path) = background_image_path {
669 if let Err(e) = renderer.load_background_image(path) {
671 log::warn!(
672 "Could not load background image '{}': {} - continuing without background image",
673 path,
674 e
675 );
676 }
677 }
678
679 Ok(renderer)
680 }
681
682 pub(crate) fn upload_solid_pixel(&mut self) {
684 let size = SOLID_PIXEL_SIZE;
685 let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
686
687 self.queue.write_texture(
688 wgpu::TexelCopyTextureInfo {
689 texture: &self.atlas.atlas_texture,
690 mip_level: 0,
691 origin: wgpu::Origin3d {
692 x: self.atlas.atlas_next_x,
693 y: self.atlas.atlas_next_y,
694 z: 0,
695 },
696 aspect: wgpu::TextureAspect::All,
697 },
698 &white_pixels,
699 wgpu::TexelCopyBufferLayout {
700 offset: 0,
701 bytes_per_row: Some(4 * size),
702 rows_per_image: Some(size),
703 },
704 wgpu::Extent3d {
705 width: size,
706 height: size,
707 depth_or_array_layers: 1,
708 },
709 );
710
711 self.atlas.solid_pixel_offset = (self.atlas.atlas_next_x, self.atlas.atlas_next_y);
712 self.atlas.atlas_next_x += size + ATLAS_GLYPH_PADDING;
713 self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(size);
714 }
715
716 pub fn device(&self) -> &wgpu::Device {
717 &self.device
718 }
719 pub fn queue(&self) -> &wgpu::Queue {
720 &self.queue
721 }
722 pub fn surface_format(&self) -> wgpu::TextureFormat {
723 self.config.format
724 }
725 pub fn keep_text_opaque(&self) -> bool {
726 self.keep_text_opaque
727 }
728
729 pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
731 let mut changed = false;
732 for row in 0..self.grid.rows {
733 let start = row * self.grid.cols;
734 let end = (row + 1) * self.grid.cols;
735 if start < new_cells.len() && end <= new_cells.len() {
736 let row_slice = &new_cells[start..end];
737 if row_slice != &self.cells[start..end] {
738 self.cells[start..end].clone_from_slice(row_slice);
739 self.dirty_rows[row] = true;
740 changed = true;
741 }
742 }
743 }
744 changed
745 }
746
747 pub fn clear_all_cells(&mut self) {
749 for cell in &mut self.cells {
750 *cell = Cell::default();
751 }
752 for dirty in &mut self.dirty_rows {
753 *dirty = true;
754 }
755 }
756
757 pub fn update_graphics(
758 &mut self,
759 _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
760 _scroll_offset: usize,
761 _scrollback_len: usize,
762 _visible_lines: usize,
763 ) -> Result<()> {
764 Ok(())
765 }
766}