1use super::cell_renderer::{Cell, CellRenderer};
2use super::graphics_renderer::GraphicsRenderer;
3use crate::custom_shader_renderer::CustomShaderRenderer;
4use anyhow::Result;
5use std::sync::Arc;
6use winit::dpi::PhysicalSize;
7use winit::window::Window;
8
9pub mod graphics;
10pub mod shaders;
11
12pub struct Renderer {
14 pub(crate) cell_renderer: CellRenderer,
16
17 pub(crate) graphics_renderer: GraphicsRenderer,
19
20 pub(crate) sixel_graphics: Vec<(u64, isize, usize, usize, usize, f32, usize)>,
23
24 pub(crate) egui_renderer: egui_wgpu::Renderer,
26
27 pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
29 pub(crate) custom_shader_path: Option<String>,
31
32 pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
34 pub(crate) cursor_shader_path: Option<String>,
36
37 pub(crate) size: PhysicalSize<u32>,
39
40 pub(crate) dirty: bool,
42
43 #[allow(dead_code)]
45 #[allow(dead_code)]
46 pub(crate) debug_text: Option<String>,
47}
48
49impl Renderer {
50 #[allow(clippy::too_many_arguments)]
52 pub async fn new(
53 window: Arc<Window>,
54 font_family: Option<&str>,
55 font_family_bold: Option<&str>,
56 font_family_italic: Option<&str>,
57 font_family_bold_italic: Option<&str>,
58 font_ranges: &[crate::config::FontRange],
59 font_size: f32,
60 window_padding: f32,
61 line_spacing: f32,
62 char_spacing: f32,
63 scrollbar_position: &str,
64 scrollbar_width: f32,
65 scrollbar_thumb_color: [f32; 4],
66 scrollbar_track_color: [f32; 4],
67 enable_text_shaping: bool,
68 enable_ligatures: bool,
69 enable_kerning: bool,
70 vsync_mode: crate::config::VsyncMode,
71 window_opacity: f32,
72 background_color: [u8; 3],
73 background_image_path: Option<&str>,
74 background_image_enabled: bool,
75 background_image_mode: crate::config::BackgroundImageMode,
76 background_image_opacity: f32,
77 custom_shader_path: Option<&str>,
78 custom_shader_enabled: bool,
79 custom_shader_animation: bool,
80 custom_shader_animation_speed: f32,
81 custom_shader_text_opacity: f32,
82 custom_shader_full_content: bool,
83 custom_shader_brightness: f32,
84 custom_shader_channel_paths: &[Option<std::path::PathBuf>; 4],
86 cursor_shader_path: Option<&str>,
88 cursor_shader_enabled: bool,
89 cursor_shader_animation: bool,
90 cursor_shader_animation_speed: f32,
91 ) -> Result<Self> {
92 let size = window.inner_size();
93 let scale_factor = window.scale_factor();
94
95 let platform_dpi = if cfg!(target_os = "macos") {
98 72.0
99 } else {
100 96.0
101 };
102
103 let base_font_pixels = font_size * platform_dpi / 72.0;
105 let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
106
107 let font_manager = crate::font_manager::FontManager::new(
109 font_family,
110 font_family_bold,
111 font_family_italic,
112 font_family_bold_italic,
113 font_ranges,
114 )?;
115
116 let (font_ascent, font_descent, font_leading, char_advance) = {
117 let primary_font = font_manager.get_font(0).unwrap();
118 let metrics = primary_font.metrics(&[]);
119 let scale = font_size_pixels / metrics.units_per_em as f32;
120
121 let glyph_id = primary_font.charmap().map('m');
123 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
124
125 (
126 metrics.ascent * scale,
127 metrics.descent * scale,
128 metrics.leading * scale,
129 advance,
130 )
131 };
132
133 let natural_line_height = font_ascent + font_descent + font_leading;
136 let char_height = (natural_line_height * line_spacing).max(1.0);
137
138 let available_width = (size.width as f32 - window_padding * 2.0).max(0.0);
140 let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
141
142 let char_width = (char_advance * char_spacing).max(1.0); let cols = (available_width / char_width).max(1.0) as usize;
145 let rows = (available_height / char_height).max(1.0) as usize;
146
147 let cell_renderer = CellRenderer::new(
149 window.clone(),
150 font_family,
151 font_family_bold,
152 font_family_italic,
153 font_family_bold_italic,
154 font_ranges,
155 font_size,
156 cols,
157 rows,
158 window_padding,
159 line_spacing,
160 char_spacing,
161 scrollbar_position,
162 scrollbar_width,
163 scrollbar_thumb_color,
164 scrollbar_track_color,
165 enable_text_shaping,
166 enable_ligatures,
167 enable_kerning,
168 vsync_mode,
169 window_opacity,
170 background_color,
171 if background_image_enabled {
172 background_image_path
173 } else {
174 None
175 },
176 background_image_mode,
177 background_image_opacity,
178 )
179 .await?;
180
181 let egui_renderer = egui_wgpu::Renderer::new(
183 cell_renderer.device(),
184 cell_renderer.surface_format(),
185 egui_wgpu::RendererOptions {
186 msaa_samples: 1,
187 depth_stencil_format: None,
188 dithering: false,
189 predictable_texture_filtering: false,
190 },
191 );
192
193 let graphics_renderer = GraphicsRenderer::new(
195 cell_renderer.device(),
196 cell_renderer.surface_format(),
197 cell_renderer.cell_width(),
198 cell_renderer.cell_height(),
199 cell_renderer.window_padding(),
200 )?;
201
202 let (custom_shader_renderer, initial_shader_path) = if custom_shader_enabled {
204 if let Some(shader_path) = custom_shader_path {
205 let path = crate::config::Config::shader_path(shader_path);
206 match CustomShaderRenderer::new(
207 cell_renderer.device(),
208 cell_renderer.queue(),
209 cell_renderer.surface_format(),
210 &path,
211 size.width,
212 size.height,
213 custom_shader_animation,
214 custom_shader_animation_speed,
215 window_opacity,
216 custom_shader_text_opacity,
217 custom_shader_full_content,
218 custom_shader_channel_paths,
219 ) {
220 Ok(mut renderer) => {
221 renderer.update_cell_dimensions(
223 cell_renderer.cell_width(),
224 cell_renderer.cell_height(),
225 window_padding,
226 );
227 renderer.set_brightness(custom_shader_brightness);
229 log::info!(
230 "Custom shader renderer initialized from: {}",
231 path.display()
232 );
233 (Some(renderer), Some(shader_path.to_string()))
234 }
235 Err(e) => {
236 log::error!("Failed to load custom shader '{}': {}", path.display(), e);
237 (None, None)
238 }
239 }
240 } else {
241 (None, None)
242 }
243 } else {
244 (None, None)
245 };
246
247 let (cursor_shader_renderer, initial_cursor_shader_path) = if cursor_shader_enabled {
249 if let Some(shader_path) = cursor_shader_path {
250 let path = crate::config::Config::shader_path(shader_path);
251 let empty_channels: [Option<std::path::PathBuf>; 4] = [None, None, None, None];
253 match CustomShaderRenderer::new(
254 cell_renderer.device(),
255 cell_renderer.queue(),
256 cell_renderer.surface_format(),
257 &path,
258 size.width,
259 size.height,
260 cursor_shader_animation,
261 cursor_shader_animation_speed,
262 window_opacity,
263 1.0, true, &empty_channels,
266 ) {
267 Ok(mut renderer) => {
268 let cell_w = cell_renderer.cell_width();
270 let cell_h = cell_renderer.cell_height();
271 renderer.update_cell_dimensions(cell_w, cell_h, window_padding);
272 log::info!(
273 "Cursor shader renderer initialized from: {} (cell={}x{}, padding={})",
274 path.display(),
275 cell_w,
276 cell_h,
277 window_padding
278 );
279 (Some(renderer), Some(shader_path.to_string()))
280 }
281 Err(e) => {
282 log::error!("Failed to load cursor shader '{}': {}", path.display(), e);
283 (None, None)
284 }
285 }
286 } else {
287 (None, None)
288 }
289 } else {
290 (None, None)
291 };
292
293 Ok(Self {
294 cell_renderer,
295 graphics_renderer,
296 sixel_graphics: Vec::new(),
297 egui_renderer,
298 custom_shader_renderer,
299 custom_shader_path: initial_shader_path,
300 cursor_shader_renderer,
301 cursor_shader_path: initial_cursor_shader_path,
302 size,
303 dirty: true, debug_text: None,
305 })
306 }
307
308 pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
310 if new_size.width > 0 && new_size.height > 0 {
311 self.size = new_size;
312 self.dirty = true; let result = self.cell_renderer.resize(new_size.width, new_size.height);
314
315 self.graphics_renderer.update_cell_dimensions(
317 self.cell_renderer.cell_width(),
318 self.cell_renderer.cell_height(),
319 self.cell_renderer.window_padding(),
320 );
321
322 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
324 custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
325 custom_shader.update_cell_dimensions(
327 self.cell_renderer.cell_width(),
328 self.cell_renderer.cell_height(),
329 self.cell_renderer.window_padding(),
330 );
331 }
332
333 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
335 cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
336 cursor_shader.update_cell_dimensions(
338 self.cell_renderer.cell_width(),
339 self.cell_renderer.cell_height(),
340 self.cell_renderer.window_padding(),
341 );
342 }
343
344 return result;
345 }
346
347 self.cell_renderer.grid_size()
348 }
349
350 pub fn handle_scale_factor_change(
352 &mut self,
353 scale_factor: f64,
354 new_size: PhysicalSize<u32>,
355 ) -> (usize, usize) {
356 self.cell_renderer.update_scale_factor(scale_factor);
357 self.resize(new_size)
358 }
359
360 pub fn update_cells(&mut self, cells: &[Cell]) {
362 self.cell_renderer.update_cells(cells);
363 self.dirty = true; }
365
366 pub fn clear_all_cells(&mut self) {
369 self.cell_renderer.clear_all_cells();
370 self.dirty = true;
371 }
372
373 pub fn update_cursor(
375 &mut self,
376 position: (usize, usize),
377 opacity: f32,
378 style: par_term_emu_core_rust::cursor::CursorStyle,
379 ) {
380 self.cell_renderer.update_cursor(position, opacity, style);
381 self.dirty = true;
382 }
383
384 pub fn clear_cursor(&mut self) {
386 self.cell_renderer.clear_cursor();
387 self.dirty = true;
388 }
389
390 pub fn update_scrollbar(
397 &mut self,
398 scroll_offset: usize,
399 visible_lines: usize,
400 total_lines: usize,
401 ) {
402 self.cell_renderer
403 .update_scrollbar(scroll_offset, visible_lines, total_lines);
404 self.dirty = true; }
406
407 pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
412 self.cell_renderer.set_visual_bell_intensity(intensity);
413 if intensity > 0.0 {
414 self.dirty = true; }
416 }
417
418 pub fn update_opacity(&mut self, opacity: f32) {
420 self.cell_renderer.update_opacity(opacity);
421
422 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
424 custom_shader.set_opacity(opacity);
425 }
426
427 self.dirty = true;
428 }
429
430 pub fn update_cursor_color(&mut self, color: [u8; 3]) {
432 self.cell_renderer.update_cursor_color(color);
433 self.dirty = true;
434 }
435
436 pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
438 self.cell_renderer.set_cursor_hidden_for_shader(hidden);
439 self.dirty = true;
440 }
441
442 pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
445 let result = self.cell_renderer.update_window_padding(padding);
446 self.dirty = true;
447 result
448 }
449
450 pub fn set_background_image_enabled(
452 &mut self,
453 enabled: bool,
454 path: Option<&str>,
455 mode: crate::config::BackgroundImageMode,
456 opacity: f32,
457 ) {
458 let path = if enabled { path } else { None };
459 self.cell_renderer.set_background_image(path, mode, opacity);
460 self.dirty = true;
461 }
462
463 pub fn update_scrollbar_appearance(
465 &mut self,
466 width: f32,
467 thumb_color: [f32; 4],
468 track_color: [f32; 4],
469 ) {
470 self.cell_renderer
471 .update_scrollbar_appearance(width, thumb_color, track_color);
472 self.dirty = true;
473 }
474
475 #[allow(dead_code)]
477 pub fn update_scrollbar_position(&mut self, position: &str) {
478 self.cell_renderer.update_scrollbar_position(position);
479 self.dirty = true;
480 }
481
482 pub fn update_background_image_opacity(&mut self, opacity: f32) {
484 self.cell_renderer.update_background_image_opacity(opacity);
485 self.dirty = true;
486 }
487
488 pub fn needs_continuous_render(&self) -> bool {
493 let custom_needs = self
494 .custom_shader_renderer
495 .as_ref()
496 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
497 let cursor_needs = self
498 .cursor_shader_renderer
499 .as_ref()
500 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
501 custom_needs || cursor_needs
502 }
503
504 pub fn render(
507 &mut self,
508 egui_data: Option<(egui::FullOutput, &egui::Context)>,
509 force_egui_opaque: bool,
510 show_scrollbar: bool,
511 ) -> Result<bool> {
512 let force_render = self.needs_continuous_render();
514
515 if !self.dirty && egui_data.is_none() && !force_render {
516 return Ok(false);
518 }
519
520 let has_custom_shader = self.custom_shader_renderer.is_some();
522 let has_cursor_shader = self.cursor_shader_renderer.is_some();
523
524 let t1 = std::time::Instant::now();
526 let surface_texture = if has_custom_shader {
527 self.cell_renderer.render_to_texture(
529 self.custom_shader_renderer
530 .as_ref()
531 .unwrap()
532 .intermediate_texture_view(),
533 )?
534 } else if has_cursor_shader {
535 self.cell_renderer.render_to_texture(
537 self.cursor_shader_renderer
538 .as_ref()
539 .unwrap()
540 .intermediate_texture_view(),
541 )?
542 } else {
543 self.cell_renderer.render(show_scrollbar)?
545 };
546 let cell_render_time = t1.elapsed();
547
548 let t_custom = std::time::Instant::now();
550 let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
551 if has_cursor_shader {
552 custom_shader.render(
554 self.cell_renderer.device(),
555 self.cell_renderer.queue(),
556 self.cursor_shader_renderer
557 .as_ref()
558 .unwrap()
559 .intermediate_texture_view(),
560 )?;
561 } else {
562 let surface_view = surface_texture
564 .texture
565 .create_view(&wgpu::TextureViewDescriptor::default());
566 custom_shader.render(
567 self.cell_renderer.device(),
568 self.cell_renderer.queue(),
569 &surface_view,
570 )?;
571
572 self.cell_renderer
574 .render_overlays(&surface_texture, show_scrollbar)?;
575 }
576 t_custom.elapsed()
577 } else {
578 std::time::Duration::ZERO
579 };
580
581 let t_cursor = std::time::Instant::now();
583 let cursor_shader_time = if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
584 log::trace!("Rendering cursor shader");
585 let surface_view = surface_texture
586 .texture
587 .create_view(&wgpu::TextureViewDescriptor::default());
588 cursor_shader.render(
589 self.cell_renderer.device(),
590 self.cell_renderer.queue(),
591 &surface_view,
592 )?;
593
594 self.cell_renderer
596 .render_overlays(&surface_texture, show_scrollbar)?;
597 t_cursor.elapsed()
598 } else {
599 std::time::Duration::ZERO
600 };
601
602 let t2 = std::time::Instant::now();
604 if !self.sixel_graphics.is_empty() {
605 self.render_sixel_graphics(&surface_texture)?;
606 }
607 let sixel_render_time = t2.elapsed();
608
609 let t3 = std::time::Instant::now();
611 if let Some((egui_output, egui_ctx)) = egui_data {
612 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
613 }
614 let egui_render_time = t3.elapsed();
615
616 let t4 = std::time::Instant::now();
618 surface_texture.present();
619 let present_time = t4.elapsed();
620
621 let total = cell_render_time
623 + custom_shader_time
624 + cursor_shader_time
625 + sixel_render_time
626 + egui_render_time
627 + present_time;
628 if present_time.as_millis() > 10 || total.as_millis() > 10 {
629 log::info!(
630 "RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
631 cell_render_time.as_secs_f64() * 1000.0,
632 custom_shader_time.as_secs_f64() * 1000.0,
633 cursor_shader_time.as_secs_f64() * 1000.0,
634 sixel_render_time.as_secs_f64() * 1000.0,
635 egui_render_time.as_secs_f64() * 1000.0,
636 present_time.as_secs_f64() * 1000.0,
637 total.as_secs_f64() * 1000.0
638 );
639 }
640
641 self.dirty = false;
643
644 Ok(true)
645 }
646
647 fn render_egui(
649 &mut self,
650 surface_texture: &wgpu::SurfaceTexture,
651 egui_output: egui::FullOutput,
652 egui_ctx: &egui::Context,
653 force_opaque: bool,
654 ) -> Result<()> {
655 use wgpu::TextureViewDescriptor;
656
657 let view = surface_texture
659 .texture
660 .create_view(&TextureViewDescriptor::default());
661
662 let mut encoder =
664 self.cell_renderer
665 .device()
666 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
667 label: Some("egui encoder"),
668 });
669
670 let screen_descriptor = egui_wgpu::ScreenDescriptor {
672 size_in_pixels: [self.size.width, self.size.height],
673 pixels_per_point: egui_output.pixels_per_point,
674 };
675
676 for (id, image_delta) in &egui_output.textures_delta.set {
678 self.egui_renderer.update_texture(
679 self.cell_renderer.device(),
680 self.cell_renderer.queue(),
681 *id,
682 image_delta,
683 );
684 }
685
686 let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
688
689 if force_opaque {
691 for job in paint_jobs.iter_mut() {
692 match &mut job.primitive {
693 egui::epaint::Primitive::Mesh(mesh) => {
694 for v in mesh.vertices.iter_mut() {
695 v.color[3] = 255;
696 }
697 }
698 egui::epaint::Primitive::Callback(_) => {}
699 }
700 }
701 }
702
703 self.egui_renderer.update_buffers(
705 self.cell_renderer.device(),
706 self.cell_renderer.queue(),
707 &mut encoder,
708 &paint_jobs,
709 &screen_descriptor,
710 );
711
712 {
714 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
715 label: Some("egui render pass"),
716 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
717 view: &view,
718 resolve_target: None,
719 ops: wgpu::Operations {
720 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
722 },
723 depth_slice: None,
724 })],
725 depth_stencil_attachment: None,
726 timestamp_writes: None,
727 occlusion_query_set: None,
728 });
729
730 let mut render_pass = render_pass.forget_lifetime();
732
733 self.egui_renderer
734 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
735 } self.cell_renderer
739 .queue()
740 .submit(std::iter::once(encoder.finish()));
741
742 for id in &egui_output.textures_delta.free {
744 self.egui_renderer.free_texture(id);
745 }
746
747 Ok(())
748 }
749
750 pub fn size(&self) -> PhysicalSize<u32> {
752 self.size
753 }
754
755 pub fn grid_size(&self) -> (usize, usize) {
757 self.cell_renderer.grid_size()
758 }
759
760 pub fn cell_width(&self) -> f32 {
762 self.cell_renderer.cell_width()
763 }
764
765 pub fn cell_height(&self) -> f32 {
767 self.cell_renderer.cell_height()
768 }
769
770 pub fn window_padding(&self) -> f32 {
772 self.cell_renderer.window_padding()
773 }
774
775 pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
781 self.cell_renderer.scrollbar_contains_point(x, y)
782 }
783
784 pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
786 self.cell_renderer.scrollbar_thumb_bounds()
787 }
788
789 pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
791 self.cell_renderer.scrollbar_track_contains_x(x)
792 }
793
794 pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
802 self.cell_renderer
803 .scrollbar_mouse_y_to_scroll_offset(mouse_y)
804 }
805
806 #[allow(dead_code)]
808 pub fn is_dirty(&self) -> bool {
809 self.dirty
810 }
811
812 #[allow(dead_code)]
814 pub fn mark_dirty(&mut self) {
815 self.dirty = true;
816 }
817
818 #[allow(dead_code)]
820 #[allow(dead_code)]
821 pub fn render_debug_overlay(&mut self, text: &str) {
822 self.debug_text = Some(text.to_string());
823 self.dirty = true; }
825
826 pub fn reconfigure_surface(&mut self) {
829 self.cell_renderer.reconfigure_surface();
830 self.dirty = true;
831 }
832
833 pub fn clear_glyph_cache(&mut self) {
836 self.cell_renderer.clear_glyph_cache();
837 self.dirty = true;
838 }
839}