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
164 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 pub(crate) scale_factor: f32,
176
177 pub(crate) font_manager: FontManager,
179 pub(crate) scrollbar: Scrollbar,
180
181 pub(crate) cells: Vec<Cell>,
183 pub(crate) dirty_rows: Vec<bool>,
184 pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
185
186 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 pub(crate) is_focused: bool,
193
194 pub(crate) bg_instances: Vec<BackgroundInstance>,
196 pub(crate) text_instances: Vec<TextInstance>,
197
198 pub(crate) scratch_row_bg: Vec<BackgroundInstance>,
200 pub(crate) scratch_row_text: Vec<TextInstance>,
201 pub(crate) scratch_row_cells: Vec<Cell>,
204
205 pub(crate) scale_context: swash::scale::ScaleContext,
210
211 pub(crate) transparency_affects_only_default_background: bool,
215 pub(crate) keep_text_opaque: bool,
217 pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
219
220 pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
222}
223
224pub struct CellRendererConfig<'a> {
229 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 #[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 let supported_present_modes = surface_caps.present_modes.clone();
347
348 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 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 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 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 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 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 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], 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 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 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 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 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 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}