1use anyhow::{Context, Result};
18use par_term_emu_core_rust::cursor::CursorStyle;
19use std::collections::BTreeMap;
20use std::path::Path;
21use std::time::Instant;
22use wgpu::util::DeviceExt;
23use wgpu::*;
24
25mod builtin_textures;
26mod cubemap;
27mod cursor;
28mod hot_reload;
29pub mod pipeline;
30mod state;
31pub mod textures;
32pub mod transpiler;
33pub mod types;
34mod uniforms;
35
36use cubemap::CubemapTexture;
37use pipeline::{
38 BindGroupInputs, create_bind_group, create_bind_group_layout, create_render_pipeline,
39};
40use textures::{ChannelTexture, load_channel_textures};
41use transpiler::transpile_glsl_to_wgsl;
42
43fn debug_shader_wgsl_filename(shader_name: &str) -> String {
50 let file_name = format!("par_term_{shader_name}_shader.wgsl");
51 #[cfg(windows)]
52 {
53 std::env::temp_dir()
54 .join(file_name)
55 .to_string_lossy()
56 .into_owned()
57 }
58 #[cfg(not(windows))]
59 {
60 format!("/tmp/{file_name}")
61 }
62}
63
64fn write_debug_shader_wgsl(shader_name: &str, wgsl_source: &str) {
65 let debug_filename = debug_shader_wgsl_filename(shader_name);
66 if let Err(e) = std::fs::write(&debug_filename, wgsl_source) {
67 log::warn!("Failed to write debug shader: {}", e);
68 } else {
69 log::info!("Wrote debug shader to {}", debug_filename);
70 }
71}
72
73fn animation_start_after_enabled_update(
74 currently_enabled: bool,
75 enabled: bool,
76 current_start: Instant,
77 now: Instant,
78) -> Instant {
79 if enabled && !currently_enabled {
80 now
81 } else {
82 current_start
83 }
84}
85
86pub struct CustomShaderRenderer {
88 pub(crate) pipeline: RenderPipeline,
90 pub(crate) bind_group: BindGroup,
92 pub(crate) uniform_buffer: Buffer,
94 pub(crate) custom_uniform_buffer: Buffer,
96 pub(crate) intermediate_texture: Texture,
98 pub(crate) intermediate_texture_view: TextureView,
100 pub(crate) start_time: Instant,
102 pub(crate) animation_enabled: bool,
104 pub(crate) animation_speed: f32,
106 pub(crate) texture_width: u32,
108 pub(crate) texture_height: u32,
109 pub(crate) surface_format: TextureFormat,
111 pub(crate) bind_group_layout: BindGroupLayout,
113 pub(crate) sampler: Sampler,
115 pub(crate) scale_factor: f32,
117 pub(crate) window_opacity: f32,
119 pub(crate) keep_text_opaque: bool,
121 pub(crate) full_content_mode: bool,
123 pub(crate) brightness: f32,
125 pub(crate) auto_dim_under_text: bool,
127 pub(crate) auto_dim_strength: f32,
129 pub(crate) frame_count: u32,
131 pub(crate) last_frame_time: Instant,
133 pub(crate) mouse_position: [f32; 2],
135 pub(crate) mouse_click_position: [f32; 2],
137 pub(crate) mouse_button_down: bool,
139 pub(crate) frame_time_accumulator: f32,
141 pub(crate) frames_in_second: u32,
143 pub(crate) current_frame_rate: f32,
145
146 pub(crate) current_cursor_pos: (usize, usize),
149 pub(crate) previous_cursor_pos: (usize, usize),
151 pub(crate) current_cursor_color: [f32; 4],
153 pub(crate) previous_cursor_color: [f32; 4],
155 pub(crate) current_cursor_opacity: f32,
157 pub(crate) previous_cursor_opacity: f32,
159 pub(crate) cursor_change_time: f32,
161 pub(crate) current_cursor_style: CursorStyle,
163 pub(crate) previous_cursor_style: CursorStyle,
165 pub(crate) cursor_cell_width: f32,
167 pub(crate) cursor_cell_height: f32,
169 pub(crate) cursor_window_padding: f32,
171 pub(crate) cursor_content_offset_y: f32,
173 pub(crate) cursor_content_offset_x: f32,
175
176 pub(crate) cursor_shader_color: [f32; 4],
179 pub(crate) cursor_trail_duration: f32,
181 pub(crate) cursor_glow_radius: f32,
183 pub(crate) cursor_glow_intensity: f32,
185
186 pub(crate) key_press_time: f32,
189
190 pub(crate) channel_textures: [ChannelTexture; 4],
193
194 pub(crate) cubemap: CubemapTexture,
197
198 pub(crate) use_background_as_channel0: bool,
201 pub(crate) background_channel_texture: Option<ChannelTexture>,
204 pub(crate) background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode,
206
207 pub(crate) background_color: [f32; 4],
212
213 pub(crate) progress_data: [f32; 4],
216
217 pub(crate) command_data: [f32; 4],
220
221 pub(crate) focused_pane: [f32; 4],
224
225 pub(crate) scroll_data: [f32; 4],
228
229 pub(crate) content_inset_right: f32,
233
234 pub(crate) custom_controls: Vec<par_term_config::ShaderControl>,
237 pub(crate) custom_uniform_values: BTreeMap<String, par_term_config::ShaderUniformValue>,
239}
240
241pub struct CustomShaderRendererConfig<'a> {
243 pub surface_format: TextureFormat,
244 pub shader_path: &'a Path,
245 pub width: u32,
246 pub height: u32,
247 pub animation_enabled: bool,
248 pub animation_speed: f32,
249 pub window_opacity: f32,
250 pub full_content_mode: bool,
251 pub channel_paths: &'a [Option<std::path::PathBuf>; 4],
252 pub cubemap_path: Option<&'a Path>,
253 pub custom_uniforms: &'a BTreeMap<String, par_term_config::ShaderUniformValue>,
254 pub background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode,
255}
256
257impl CustomShaderRenderer {
258 pub fn new(
260 device: &Device,
261 queue: &Queue,
262 config: CustomShaderRendererConfig<'_>,
263 ) -> Result<Self> {
264 let CustomShaderRendererConfig {
265 surface_format,
266 shader_path,
267 width,
268 height,
269 animation_enabled,
270 animation_speed,
271 window_opacity,
272 full_content_mode,
273 channel_paths,
274 cubemap_path,
275 custom_uniforms,
276 background_channel0_blend_mode,
277 } = config;
278 let glsl_source = std::fs::read_to_string(shader_path)
280 .with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
281
282 let control_parse = par_term_config::parse_shader_controls(&glsl_source);
283 for warning in &control_parse.warnings {
284 log::warn!(
285 "Shader control warning line {}: {}",
286 warning.line,
287 warning.message
288 );
289 }
290 let custom_controls = control_parse.controls;
291 let custom_uniform_values = custom_uniforms.clone();
292
293 let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
295
296 log::info!(
297 "Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
298 shader_path.display(),
299 glsl_source.len(),
300 wgsl_source.len()
301 );
302 log::debug!("Generated WGSL:\n{}", wgsl_source);
303
304 let shader_name = shader_path
306 .file_stem()
307 .and_then(|s| s.to_str())
308 .unwrap_or("unknown");
309 write_debug_shader_wgsl(shader_name, &wgsl_source);
310
311 let module = naga::front::wgsl::parse_str(&wgsl_source)
313 .context("Custom shader WGSL parse failed")?;
314 let _info = naga::valid::Validator::new(
315 naga::valid::ValidationFlags::all(),
316 naga::valid::Capabilities::empty(),
317 )
318 .validate(&module)
319 .context("Custom shader WGSL validation failed")?;
320
321 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
322 label: Some("Custom Shader Module"),
323 source: ShaderSource::Wgsl(wgsl_source.clone().into()),
324 });
325
326 let (intermediate_texture, intermediate_texture_view) =
328 Self::create_intermediate_texture(device, surface_format, width, height);
329
330 let sampler = device.create_sampler(&SamplerDescriptor {
333 label: Some("Custom Shader Sampler"),
334 address_mode_u: AddressMode::ClampToEdge,
335 address_mode_v: AddressMode::ClampToEdge,
336 address_mode_w: AddressMode::ClampToEdge,
337 mag_filter: FilterMode::Nearest,
338 min_filter: FilterMode::Nearest,
339 mipmap_filter: MipmapFilterMode::Nearest,
340 ..Default::default()
341 });
342
343 let channel_textures = load_channel_textures(device, queue, channel_paths);
345
346 let cubemap = match cubemap_path {
348 Some(path) => match CubemapTexture::from_prefix(device, queue, path) {
349 Ok(cm) => cm,
350 Err(e) => {
351 log::error!("Failed to load cubemap '{}': {}", path.display(), e);
352 CubemapTexture::placeholder(device, queue)
353 }
354 },
355 None => CubemapTexture::placeholder(device, queue),
356 };
357
358 let uniform_buffer = Self::create_uniform_buffer(device);
360 let custom_uniform_data =
361 crate::custom_shader_renderer::types::CustomShaderControlUniforms::from_controls(
362 &custom_controls,
363 &custom_uniform_values,
364 );
365 let custom_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
366 label: Some("Custom Shader Control Uniform Buffer"),
367 contents: bytemuck::cast_slice(&[custom_uniform_data]),
368 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
369 });
370
371 let bind_group_layout = create_bind_group_layout(device);
373 let bind_group = create_bind_group(
374 device,
375 BindGroupInputs {
376 layout: &bind_group_layout,
377 uniform_buffer: &uniform_buffer,
378 intermediate_texture_view: &intermediate_texture_view,
379 custom_uniform_buffer: &custom_uniform_buffer,
380 sampler: &sampler,
381 channel_textures: &channel_textures,
382 cubemap: &cubemap,
383 },
384 );
385
386 let pipeline = create_render_pipeline(
388 device,
389 &shader_module,
390 &bind_group_layout,
391 surface_format,
392 Some("Custom Shader Pipeline"),
393 );
394
395 let now = Instant::now();
396 Ok(Self {
397 pipeline,
398 bind_group,
399 uniform_buffer,
400 custom_uniform_buffer,
401 intermediate_texture,
402 intermediate_texture_view,
403 start_time: now,
404 animation_enabled,
405 animation_speed,
406 texture_width: width,
407 texture_height: height,
408 surface_format,
409 bind_group_layout,
410 sampler,
411 window_opacity,
412 keep_text_opaque: false,
413 scale_factor: 1.0,
414 full_content_mode,
415 brightness: 1.0,
416 auto_dim_under_text: false,
417 auto_dim_strength: 0.35,
418 frame_count: 0,
419 last_frame_time: now,
420 mouse_position: [0.0, 0.0],
421 mouse_click_position: [0.0, 0.0],
422 mouse_button_down: false,
423 frame_time_accumulator: 0.0,
424 frames_in_second: 0,
425 current_frame_rate: 60.0,
426 current_cursor_pos: (0, 0),
427 previous_cursor_pos: (0, 0),
428 current_cursor_color: [1.0, 1.0, 1.0, 1.0],
429 previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
430 current_cursor_opacity: 1.0,
431 previous_cursor_opacity: 1.0,
432 cursor_change_time: 0.0,
433 current_cursor_style: CursorStyle::SteadyBlock,
434 previous_cursor_style: CursorStyle::SteadyBlock,
435 cursor_cell_width: 10.0,
436 cursor_cell_height: 20.0,
437 cursor_window_padding: 0.0,
438 cursor_content_offset_y: 0.0,
439 cursor_content_offset_x: 0.0,
440 cursor_shader_color: [1.0, 1.0, 1.0, 1.0],
441 cursor_trail_duration: 0.5,
442 cursor_glow_radius: 80.0,
443 cursor_glow_intensity: 0.3,
444 key_press_time: 0.0,
445 channel_textures,
446 cubemap,
447 use_background_as_channel0: false,
448 background_channel_texture: None,
449 background_channel0_blend_mode,
450 background_color: [0.0, 0.0, 0.0, 0.0], progress_data: [0.0, 0.0, 0.0, 0.0],
452 command_data: [0.0, 0.0, 0.0, 0.0],
453 focused_pane: [0.0, 0.0, width as f32, height as f32],
454 scroll_data: [0.0, 0.0, 0.0, 0.0],
455 content_inset_right: 0.0,
456 custom_controls,
457 custom_uniform_values,
458 })
459 }
460
461 pub fn intermediate_texture_view(&self) -> &TextureView {
463 &self.intermediate_texture_view
464 }
465
466 pub fn render(
476 &mut self,
477 device: &Device,
478 queue: &Queue,
479 output_view: &TextureView,
480 apply_opacity: bool,
481 ) -> Result<()> {
482 self.render_with_clear_color(
483 device,
484 queue,
485 output_view,
486 apply_opacity,
487 Color::TRANSPARENT,
488 )
489 }
490
491 pub fn render_with_clear_color(
494 &mut self,
495 device: &Device,
496 queue: &Queue,
497 output_view: &TextureView,
498 apply_opacity: bool,
499 clear_color: Color,
500 ) -> Result<()> {
501 let now = Instant::now();
502
503 let time = if self.animation_enabled {
505 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
506 } else {
507 0.0
508 };
509
510 let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
512 self.last_frame_time = now;
513
514 self.frame_time_accumulator += time_delta;
516 self.frames_in_second += 1;
517 if self.frame_time_accumulator >= 1.0 {
518 self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
519 self.frame_time_accumulator = 0.0;
520 self.frames_in_second = 0;
521 }
522
523 self.frame_count = self.frame_count.wrapping_add(1);
524
525 let uniforms = self.build_uniforms(time, time_delta, apply_opacity);
527 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
528 let custom_uniforms =
529 crate::custom_shader_renderer::types::CustomShaderControlUniforms::from_controls(
530 &self.custom_controls,
531 &self.custom_uniform_values,
532 );
533 queue.write_buffer(
534 &self.custom_uniform_buffer,
535 0,
536 bytemuck::cast_slice(&[custom_uniforms]),
537 );
538
539 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
541 label: Some("Custom Shader Encoder"),
542 });
543
544 {
545 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
546 label: Some("Custom Shader Render Pass"),
547 color_attachments: &[Some(RenderPassColorAttachment {
548 view: output_view,
549 resolve_target: None,
550 ops: Operations {
551 load: LoadOp::Clear(clear_color),
552 store: StoreOp::Store,
553 },
554 depth_slice: None,
555 })],
556 depth_stencil_attachment: None,
557 timestamp_writes: None,
558 occlusion_query_set: None,
559 multiview_mask: None,
560 });
561
562 render_pass.set_pipeline(&self.pipeline);
568 render_pass.set_bind_group(0, &self.bind_group, &[]);
569 render_pass.draw(0..4, 0..1);
570 }
571
572 queue.submit(std::iter::once(encoder.finish()));
573 Ok(())
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use std::time::Duration;
581
582 #[test]
583 fn enabling_animation_when_already_enabled_preserves_start_time() {
584 let start_time = Instant::now() - Duration::from_secs(5);
585 let now = Instant::now();
586
587 assert_eq!(
588 animation_start_after_enabled_update(true, true, start_time, now),
589 start_time
590 );
591 }
592
593 #[test]
594 fn enabling_animation_from_disabled_starts_at_now() {
595 let start_time = Instant::now() - Duration::from_secs(5);
596 let now = Instant::now();
597
598 assert_eq!(
599 animation_start_after_enabled_update(false, true, start_time, now),
600 now
601 );
602 }
603
604 #[test]
605 fn debug_shader_wgsl_filename_matches_new_renderer_output_path() {
606 let path = debug_shader_wgsl_filename("matrix");
607 #[cfg(not(windows))]
608 assert_eq!(path, "/tmp/par_term_matrix_shader.wgsl");
609 #[cfg(windows)]
610 assert_eq!(
611 std::path::Path::new(&path),
612 std::env::temp_dir().join("par_term_matrix_shader.wgsl")
613 );
614 }
615
616 #[test]
617 fn write_debug_shader_wgsl_refreshes_existing_output() {
618 let shader_name = format!("par_term_test_{}", std::process::id());
619 let path = debug_shader_wgsl_filename(&shader_name);
620 let _ = std::fs::remove_file(&path);
621
622 write_debug_shader_wgsl(&shader_name, "first");
623 write_debug_shader_wgsl(&shader_name, "second");
624
625 assert_eq!(
626 std::fs::read_to_string(&path).expect("read debug wgsl"),
627 "second"
628 );
629 std::fs::remove_file(&path).expect("remove debug wgsl");
630 }
631}