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, pane_instance_capacity};
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 pub(crate) pane_bg_cursor: usize,
125 pub(crate) pane_text_cursor: usize,
128 pub(crate) overflow_reported: bool,
131}
132
133pub(crate) struct GlyphAtlas {
135 pub(crate) atlas_texture: wgpu::Texture,
136 #[allow(dead_code)] 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 pub(crate) atlas_size: u32,
146 pub(crate) solid_pixel_offset: (u32, u32),
148}
149
150pub(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 pub(crate) bg_is_solid_color: bool,
161 pub(crate) solid_bg_color: [f32; 3],
164 pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
166 pub(crate) pane_bg_uniform_cache: HashMap<usize, background::PaneBgUniformEntry>,
175}
176
177pub(crate) struct SeparatorConfig {
179 pub(crate) enabled: bool,
181 pub(crate) thickness: f32,
183 pub(crate) opacity: f32,
185 pub(crate) exit_color: bool,
187 pub(crate) color: [f32; 3],
189 pub(crate) visible_marks: Vec<SeparatorMark>,
191}
192
193pub struct CellRenderer {
194 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 pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
201
202 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 pub(crate) scale_factor: f32,
214
215 pub(crate) font_manager: FontManager,
217 pub(crate) scrollbar: Scrollbar,
218
219 pub(crate) cells: Vec<Cell>,
221 pub(crate) dirty_rows: Vec<bool>,
222 pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
223
224 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 pub(crate) is_focused: bool,
231
232 pub(crate) bg_instances: Vec<BackgroundInstance>,
234 pub(crate) text_instances: Vec<TextInstance>,
235
236 pub(crate) scratch_row_bg: Vec<BackgroundInstance>,
238 pub(crate) scratch_row_text: Vec<TextInstance>,
239 pub(crate) scratch_row_cells: Vec<Cell>,
242
243 pub(crate) scale_context: swash::scale::ScaleContext,
248
249 pub(crate) transparency_affects_only_default_background: bool,
253 pub(crate) keep_text_opaque: bool,
255 pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
257
258 pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
260}
261
262pub struct CellRendererConfig<'a> {
267 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 #[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 })
355 .await
356 .context("Failed to find wgpu adapter")?;
357
358 let (device, queue) = adapter
359 .request_device(&wgpu::DeviceDescriptor {
360 label: Some("device"),
361 required_features: wgpu::Features::empty(),
362 required_limits: wgpu::Limits::default(),
363 memory_hints: wgpu::MemoryHints::default(),
364 ..Default::default()
365 })
366 .await?;
367
368 let device = Arc::new(device);
369 let queue = Arc::new(queue);
370
371 let size = window.inner_size();
372 let surface_caps = surface.get_capabilities(&adapter);
373 let surface_format = surface_caps
374 .formats
375 .iter()
376 .copied()
377 .find(|f| !f.is_srgb())
378 .or_else(|| surface_caps.formats.first().copied())
379 .context("Surface reports no supported texture formats")?;
380
381 let supported_present_modes = surface_caps.present_modes.clone();
383
384 let requested_mode = vsync_mode.to_present_mode();
386 let present_mode = if supported_present_modes.contains(&requested_mode) {
387 requested_mode
388 } else {
389 log::warn!(
391 "Requested present mode {:?} not supported (available: {:?}), falling back",
392 requested_mode,
393 supported_present_modes
394 );
395 if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
396 wgpu::PresentMode::Fifo
397 } else {
398 supported_present_modes
399 .first()
400 .copied()
401 .context("Surface reports no supported present modes")?
402 }
403 };
404
405 let alpha_mode = if surface_caps
408 .alpha_modes
409 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
410 {
411 wgpu::CompositeAlphaMode::PreMultiplied
412 } else if surface_caps
413 .alpha_modes
414 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
415 {
416 wgpu::CompositeAlphaMode::PostMultiplied
417 } else if surface_caps
418 .alpha_modes
419 .contains(&wgpu::CompositeAlphaMode::Auto)
420 {
421 wgpu::CompositeAlphaMode::Auto
422 } else {
423 surface_caps
424 .alpha_modes
425 .first()
426 .copied()
427 .context("Surface reports no supported alpha modes")?
428 };
429 log::info!(
430 "Selected alpha mode: {:?} (available: {:?})",
431 alpha_mode,
432 surface_caps.alpha_modes
433 );
434
435 let config = wgpu::SurfaceConfiguration {
436 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
437 format: surface_format,
438 width: size.width.max(1),
439 height: size.height.max(1),
440 present_mode,
441 alpha_mode,
442 view_formats: vec![],
443 desired_maximum_frame_latency: SURFACE_FRAME_LATENCY,
444 };
445 surface.configure(&device, &config);
446
447 let scale_factor = window.scale_factor() as f32;
448
449 let platform_dpi = if cfg!(target_os = "macos") {
450 MACOS_PLATFORM_DPI
451 } else {
452 DEFAULT_PLATFORM_DPI
453 };
454
455 let base_font_pixels = font_size * platform_dpi / FONT_REFERENCE_DPI;
456 let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
457
458 let (font_ascent, font_descent, font_leading, char_advance) = {
460 let primary_font = font_manager
461 .get_font(0)
462 .expect("Primary font at index 0 must exist after FontManager initialization");
463 let metrics = primary_font.metrics(&[]);
464 let scale = font_size_pixels / metrics.units_per_em as f32;
465 let glyph_id = primary_font.charmap().map('m');
466 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
467 (
468 metrics.ascent * scale,
469 metrics.descent * scale,
470 metrics.leading * scale,
471 advance,
472 )
473 };
474
475 let natural_line_height = font_ascent + font_descent + font_leading;
476 let cell_height = (natural_line_height * line_spacing).max(1.0).round();
481 let cell_width = (char_advance * char_spacing).max(1.0).round();
482
483 let scrollbar = Scrollbar::new(
484 Arc::clone(&device),
485 surface_format,
486 scrollbar_width,
487 scrollbar_position,
488 scrollbar_thumb_color,
489 scrollbar_track_color,
490 );
491
492 let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
494
495 let (atlas_texture, atlas_view, atlas_sampler, atlas_size) =
496 pipeline::create_atlas(&device);
497 let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
498 let text_bind_group = pipeline::create_text_bind_group(
499 &device,
500 &text_bind_group_layout,
501 &atlas_view,
502 &atlas_sampler,
503 );
504 let text_pipeline =
505 pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
506
507 let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
508 let bg_image_pipeline = pipeline::create_bg_image_pipeline(
509 &device,
510 surface_format,
511 &bg_image_bind_group_layout,
512 );
513 let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
514
515 let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
516 pipeline::create_visual_bell_pipeline(&device, surface_format);
517
518 let opaque_alpha_pipeline = pipeline::create_opaque_alpha_pipeline(&device, surface_format);
519
520 let vertex_buffer = pipeline::create_vertex_buffer(&device);
521
522 let max_bg_instances = render::SingleGridLayout::new(cols, rows).bg_instances();
524 let max_text_instances = cols * rows * TEXT_INSTANCES_PER_CELL;
525 let (bg_instance_buffer, text_instance_buffer) =
526 pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
527
528 let mut renderer = Self {
529 device,
530 queue,
531 surface,
532 config,
533 supported_present_modes,
534 pipelines: GpuPipelines {
535 bg_pipeline,
536 text_pipeline,
537 bg_image_pipeline,
538 visual_bell_pipeline,
539 text_bind_group,
540 text_bind_group_layout,
541 bg_image_bind_group: None,
542 bg_image_bind_group_layout,
543 visual_bell_bind_group,
544 opaque_alpha_pipeline,
545 },
546 buffers: GpuBuffers {
547 vertex_buffer,
548 bg_instance_buffer,
549 text_instance_buffer,
550 bg_image_uniform_buffer,
551 visual_bell_uniform_buffer,
552 max_bg_instances,
553 max_text_instances,
554 actual_bg_instances: 0,
555 actual_text_instances: 0,
556 pane_bg_cursor: 0,
557 pane_text_cursor: 0,
558 overflow_reported: false,
559 },
560 atlas: GlyphAtlas {
561 atlas_texture,
562 atlas_view,
563 glyph_cache: HashMap::new(),
564 lru_head: None,
565 lru_tail: None,
566 atlas_next_x: 0,
567 atlas_next_y: 0,
568 atlas_row_height: 0,
569 atlas_size,
570 solid_pixel_offset: (0, 0),
571 },
572 grid: GridLayout {
573 cols,
574 rows,
575 cell_width,
576 cell_height,
577 window_padding,
578 content_offset_y: 0.0,
579 content_offset_x: 0.0,
580 content_inset_bottom: 0.0,
581 content_inset_right: 0.0,
582 egui_bottom_inset: 0.0,
583 egui_right_inset: 0.0,
584 },
585 cursor: CursorState {
586 pos: (0, 0),
587 opacity: 0.0,
588 style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
589 color: [1.0, 1.0, 1.0],
590 text_color: None,
591 hidden_for_shader: false,
592 guide_enabled: false,
593 guide_color: [1.0, 1.0, 1.0, DEFAULT_GUIDE_OPACITY],
594 shadow_enabled: false,
595 shadow_color: [0.0, 0.0, 0.0, DEFAULT_SHADOW_ALPHA],
596 shadow_offset: [DEFAULT_SHADOW_OFFSET_PX, DEFAULT_SHADOW_OFFSET_PX],
597 shadow_blur: DEFAULT_SHADOW_BLUR_PX,
598 boost: 0.0,
599 boost_color: [1.0, 1.0, 1.0],
600 unfocused_style: par_term_config::UnfocusedCursorStyle::default(),
601 },
602 font: FontState {
603 base_font_size: font_size,
604 line_spacing,
605 char_spacing,
606 font_ascent,
607 font_descent,
608 font_leading,
609 font_size_pixels,
610 char_advance,
611 enable_text_shaping,
612 enable_ligatures,
613 enable_kerning,
614 font_antialias,
615 font_hinting,
616 font_thin_strokes,
617 minimum_contrast: minimum_contrast.clamp(0.0, 1.0),
618 },
619 bg_state: BackgroundImageState {
620 bg_image_texture: None,
621 bg_image_mode: background_image_mode,
622 bg_image_opacity: background_image_opacity,
623 bg_image_width: 0,
624 bg_image_height: 0,
625 bg_is_solid_color: false,
626 solid_bg_color: [0.0, 0.0, 0.0],
627 pane_bg_cache: HashMap::new(),
628 pane_bg_uniform_cache: HashMap::new(),
629 },
630 separator: SeparatorConfig {
631 enabled: false,
632 thickness: 1.0,
633 opacity: 0.4,
634 exit_color: true,
635 color: [0.5, 0.5, 0.5],
636 visible_marks: Vec::new(),
637 },
638 scale_factor,
639 font_manager,
640 scrollbar,
641 cells: vec![Cell::default(); cols * rows],
642 dirty_rows: vec![true; rows],
643 row_cache: (0..rows).map(|_| None).collect(),
644 is_focused: true,
645 visual_bell_intensity: 0.0,
646 visual_bell_color: [1.0, 1.0, 1.0], window_opacity,
648 background_color: color_u8_to_f32_a(background_color, 1.0),
649 bg_instances: vec![
650 BackgroundInstance {
651 position: [0.0, 0.0],
652 size: [0.0, 0.0],
653 color: [0.0, 0.0, 0.0, 0.0],
654 };
655 max_bg_instances
656 ],
657 text_instances: vec![
658 TextInstance {
659 position: [0.0, 0.0],
660 size: [0.0, 0.0],
661 tex_offset: [0.0, 0.0],
662 tex_size: [0.0, 0.0],
663 color: [0.0, 0.0, 0.0, 0.0],
664 is_colored: 0,
665 };
666 max_text_instances
667 ],
668 transparency_affects_only_default_background: false,
669 keep_text_opaque: true,
670 link_underline_style: par_term_config::LinkUnderlineStyle::default(),
671 gutter_indicators: Vec::new(),
672 scratch_row_bg: Vec::with_capacity(cols),
673 scratch_row_text: Vec::with_capacity(cols * 2),
674 scratch_row_cells: Vec::with_capacity(cols),
675 scale_context: swash::scale::ScaleContext::new(),
676 };
677
678 renderer.upload_solid_pixel();
680
681 log::info!(
682 "CellRenderer::new: background_image_path={:?}",
683 background_image_path
684 );
685 if let Some(path) = background_image_path {
686 if let Err(e) = renderer.load_background_image(path) {
688 log::warn!(
689 "Could not load background image '{}': {} - continuing without background image",
690 path,
691 e
692 );
693 }
694 }
695
696 Ok(renderer)
697 }
698
699 pub(crate) fn upload_solid_pixel(&mut self) {
701 let size = SOLID_PIXEL_SIZE;
702 let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
703
704 self.queue.write_texture(
705 wgpu::TexelCopyTextureInfo {
706 texture: &self.atlas.atlas_texture,
707 mip_level: 0,
708 origin: wgpu::Origin3d {
709 x: self.atlas.atlas_next_x,
710 y: self.atlas.atlas_next_y,
711 z: 0,
712 },
713 aspect: wgpu::TextureAspect::All,
714 },
715 &white_pixels,
716 wgpu::TexelCopyBufferLayout {
717 offset: 0,
718 bytes_per_row: Some(4 * size),
719 rows_per_image: Some(size),
720 },
721 wgpu::Extent3d {
722 width: size,
723 height: size,
724 depth_or_array_layers: 1,
725 },
726 );
727
728 self.atlas.solid_pixel_offset = (self.atlas.atlas_next_x, self.atlas.atlas_next_y);
729 self.atlas.atlas_next_x += size + ATLAS_GLYPH_PADDING;
730 self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(size);
731 }
732
733 pub fn device(&self) -> &wgpu::Device {
734 &self.device
735 }
736 pub fn queue(&self) -> &wgpu::Queue {
737 &self.queue
738 }
739 pub fn surface_format(&self) -> wgpu::TextureFormat {
740 self.config.format
741 }
742 pub fn keep_text_opaque(&self) -> bool {
743 self.keep_text_opaque
744 }
745
746 pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
754 let n = new_cells.len().min(self.cells.len());
755 let mut changed = false;
756 for row in 0..self.grid.rows {
757 let start = row * self.grid.cols;
758 if start >= n {
759 break;
760 }
761 let end = ((row + 1) * self.grid.cols).min(n);
762 let row_slice = &new_cells[start..end];
763 if row_slice != &self.cells[start..end] {
764 self.cells[start..end].clone_from_slice(row_slice);
765 self.dirty_rows[row] = true;
766 changed = true;
767 }
768 }
769 changed
770 }
771
772 pub fn clear_all_cells(&mut self) {
774 for cell in &mut self.cells {
775 *cell = Cell::default();
776 }
777 for dirty in &mut self.dirty_rows {
778 *dirty = true;
779 }
780 }
781
782 pub fn update_graphics(
783 &mut self,
784 _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
785 _scroll_offset: usize,
786 _scrollback_len: usize,
787 _visible_lines: usize,
788 ) -> Result<()> {
789 Ok(())
790 }
791}