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