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 {
44 format!("/tmp/par_term_{shader_name}_shader.wgsl")
45}
46
47fn write_debug_shader_wgsl(shader_name: &str, wgsl_source: &str) {
48 let debug_filename = debug_shader_wgsl_filename(shader_name);
49 if let Err(e) = std::fs::write(&debug_filename, wgsl_source) {
50 log::warn!("Failed to write debug shader: {}", e);
51 } else {
52 log::info!("Wrote debug shader to {}", debug_filename);
53 }
54}
55
56fn animation_start_after_enabled_update(
57 currently_enabled: bool,
58 enabled: bool,
59 current_start: Instant,
60 now: Instant,
61) -> Instant {
62 if enabled && !currently_enabled {
63 now
64 } else {
65 current_start
66 }
67}
68
69pub struct CustomShaderRenderer {
71 pub(crate) pipeline: RenderPipeline,
73 pub(crate) bind_group: BindGroup,
75 pub(crate) uniform_buffer: Buffer,
77 pub(crate) custom_uniform_buffer: Buffer,
79 pub(crate) intermediate_texture: Texture,
81 pub(crate) intermediate_texture_view: TextureView,
83 pub(crate) start_time: Instant,
85 pub(crate) animation_enabled: bool,
87 pub(crate) animation_speed: f32,
89 pub(crate) texture_width: u32,
91 pub(crate) texture_height: u32,
92 pub(crate) surface_format: TextureFormat,
94 pub(crate) bind_group_layout: BindGroupLayout,
96 pub(crate) sampler: Sampler,
98 pub(crate) scale_factor: f32,
100 pub(crate) window_opacity: f32,
102 pub(crate) keep_text_opaque: bool,
104 pub(crate) full_content_mode: bool,
106 pub(crate) brightness: f32,
108 pub(crate) auto_dim_under_text: bool,
110 pub(crate) auto_dim_strength: f32,
112 pub(crate) frame_count: u32,
114 pub(crate) last_frame_time: Instant,
116 pub(crate) mouse_position: [f32; 2],
118 pub(crate) mouse_click_position: [f32; 2],
120 pub(crate) mouse_button_down: bool,
122 pub(crate) frame_time_accumulator: f32,
124 pub(crate) frames_in_second: u32,
126 pub(crate) current_frame_rate: f32,
128
129 pub(crate) current_cursor_pos: (usize, usize),
132 pub(crate) previous_cursor_pos: (usize, usize),
134 pub(crate) current_cursor_color: [f32; 4],
136 pub(crate) previous_cursor_color: [f32; 4],
138 pub(crate) current_cursor_opacity: f32,
140 pub(crate) previous_cursor_opacity: f32,
142 pub(crate) cursor_change_time: f32,
144 pub(crate) current_cursor_style: CursorStyle,
146 pub(crate) previous_cursor_style: CursorStyle,
148 pub(crate) cursor_cell_width: f32,
150 pub(crate) cursor_cell_height: f32,
152 pub(crate) cursor_window_padding: f32,
154 pub(crate) cursor_content_offset_y: f32,
156 pub(crate) cursor_content_offset_x: f32,
158
159 pub(crate) cursor_shader_color: [f32; 4],
162 pub(crate) cursor_trail_duration: f32,
164 pub(crate) cursor_glow_radius: f32,
166 pub(crate) cursor_glow_intensity: f32,
168
169 pub(crate) key_press_time: f32,
172
173 pub(crate) channel_textures: [ChannelTexture; 4],
176
177 pub(crate) cubemap: CubemapTexture,
180
181 pub(crate) use_background_as_channel0: bool,
184 pub(crate) background_channel_texture: Option<ChannelTexture>,
187 pub(crate) background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode,
189
190 pub(crate) background_color: [f32; 4],
195
196 pub(crate) progress_data: [f32; 4],
199
200 pub(crate) command_data: [f32; 4],
203
204 pub(crate) focused_pane: [f32; 4],
207
208 pub(crate) scroll_data: [f32; 4],
211
212 pub(crate) content_inset_right: f32,
216
217 pub(crate) custom_controls: Vec<par_term_config::ShaderControl>,
220 pub(crate) custom_uniform_values: BTreeMap<String, par_term_config::ShaderUniformValue>,
222}
223
224pub struct CustomShaderRendererConfig<'a> {
226 pub surface_format: TextureFormat,
227 pub shader_path: &'a Path,
228 pub width: u32,
229 pub height: u32,
230 pub animation_enabled: bool,
231 pub animation_speed: f32,
232 pub window_opacity: f32,
233 pub full_content_mode: bool,
234 pub channel_paths: &'a [Option<std::path::PathBuf>; 4],
235 pub cubemap_path: Option<&'a Path>,
236 pub custom_uniforms: &'a BTreeMap<String, par_term_config::ShaderUniformValue>,
237 pub background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode,
238}
239
240impl CustomShaderRenderer {
241 pub fn new(
243 device: &Device,
244 queue: &Queue,
245 config: CustomShaderRendererConfig<'_>,
246 ) -> Result<Self> {
247 let CustomShaderRendererConfig {
248 surface_format,
249 shader_path,
250 width,
251 height,
252 animation_enabled,
253 animation_speed,
254 window_opacity,
255 full_content_mode,
256 channel_paths,
257 cubemap_path,
258 custom_uniforms,
259 background_channel0_blend_mode,
260 } = config;
261 let glsl_source = std::fs::read_to_string(shader_path)
263 .with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
264
265 let control_parse = par_term_config::parse_shader_controls(&glsl_source);
266 for warning in &control_parse.warnings {
267 log::warn!(
268 "Shader control warning line {}: {}",
269 warning.line,
270 warning.message
271 );
272 }
273 let custom_controls = control_parse.controls;
274 let custom_uniform_values = custom_uniforms.clone();
275
276 let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
278
279 log::info!(
280 "Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
281 shader_path.display(),
282 glsl_source.len(),
283 wgsl_source.len()
284 );
285 log::debug!("Generated WGSL:\n{}", wgsl_source);
286
287 let shader_name = shader_path
289 .file_stem()
290 .and_then(|s| s.to_str())
291 .unwrap_or("unknown");
292 write_debug_shader_wgsl(shader_name, &wgsl_source);
293
294 let module = naga::front::wgsl::parse_str(&wgsl_source)
296 .context("Custom shader WGSL parse failed")?;
297 let _info = naga::valid::Validator::new(
298 naga::valid::ValidationFlags::all(),
299 naga::valid::Capabilities::empty(),
300 )
301 .validate(&module)
302 .context("Custom shader WGSL validation failed")?;
303
304 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
305 label: Some("Custom Shader Module"),
306 source: ShaderSource::Wgsl(wgsl_source.clone().into()),
307 });
308
309 let (intermediate_texture, intermediate_texture_view) =
311 Self::create_intermediate_texture(device, surface_format, width, height);
312
313 let sampler = device.create_sampler(&SamplerDescriptor {
316 label: Some("Custom Shader Sampler"),
317 address_mode_u: AddressMode::ClampToEdge,
318 address_mode_v: AddressMode::ClampToEdge,
319 address_mode_w: AddressMode::ClampToEdge,
320 mag_filter: FilterMode::Nearest,
321 min_filter: FilterMode::Nearest,
322 mipmap_filter: MipmapFilterMode::Nearest,
323 ..Default::default()
324 });
325
326 let channel_textures = load_channel_textures(device, queue, channel_paths);
328
329 let cubemap = match cubemap_path {
331 Some(path) => match CubemapTexture::from_prefix(device, queue, path) {
332 Ok(cm) => cm,
333 Err(e) => {
334 log::error!("Failed to load cubemap '{}': {}", path.display(), e);
335 CubemapTexture::placeholder(device, queue)
336 }
337 },
338 None => CubemapTexture::placeholder(device, queue),
339 };
340
341 let uniform_buffer = Self::create_uniform_buffer(device);
343 let custom_uniform_data =
344 crate::custom_shader_renderer::types::CustomShaderControlUniforms::from_controls(
345 &custom_controls,
346 &custom_uniform_values,
347 );
348 let custom_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
349 label: Some("Custom Shader Control Uniform Buffer"),
350 contents: bytemuck::cast_slice(&[custom_uniform_data]),
351 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
352 });
353
354 let bind_group_layout = create_bind_group_layout(device);
356 let bind_group = create_bind_group(
357 device,
358 BindGroupInputs {
359 layout: &bind_group_layout,
360 uniform_buffer: &uniform_buffer,
361 intermediate_texture_view: &intermediate_texture_view,
362 custom_uniform_buffer: &custom_uniform_buffer,
363 sampler: &sampler,
364 channel_textures: &channel_textures,
365 cubemap: &cubemap,
366 },
367 );
368
369 let pipeline = create_render_pipeline(
371 device,
372 &shader_module,
373 &bind_group_layout,
374 surface_format,
375 Some("Custom Shader Pipeline"),
376 );
377
378 let now = Instant::now();
379 Ok(Self {
380 pipeline,
381 bind_group,
382 uniform_buffer,
383 custom_uniform_buffer,
384 intermediate_texture,
385 intermediate_texture_view,
386 start_time: now,
387 animation_enabled,
388 animation_speed,
389 texture_width: width,
390 texture_height: height,
391 surface_format,
392 bind_group_layout,
393 sampler,
394 window_opacity,
395 keep_text_opaque: false,
396 scale_factor: 1.0,
397 full_content_mode,
398 brightness: 1.0,
399 auto_dim_under_text: false,
400 auto_dim_strength: 0.35,
401 frame_count: 0,
402 last_frame_time: now,
403 mouse_position: [0.0, 0.0],
404 mouse_click_position: [0.0, 0.0],
405 mouse_button_down: false,
406 frame_time_accumulator: 0.0,
407 frames_in_second: 0,
408 current_frame_rate: 60.0,
409 current_cursor_pos: (0, 0),
410 previous_cursor_pos: (0, 0),
411 current_cursor_color: [1.0, 1.0, 1.0, 1.0],
412 previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
413 current_cursor_opacity: 1.0,
414 previous_cursor_opacity: 1.0,
415 cursor_change_time: 0.0,
416 current_cursor_style: CursorStyle::SteadyBlock,
417 previous_cursor_style: CursorStyle::SteadyBlock,
418 cursor_cell_width: 10.0,
419 cursor_cell_height: 20.0,
420 cursor_window_padding: 0.0,
421 cursor_content_offset_y: 0.0,
422 cursor_content_offset_x: 0.0,
423 cursor_shader_color: [1.0, 1.0, 1.0, 1.0],
424 cursor_trail_duration: 0.5,
425 cursor_glow_radius: 80.0,
426 cursor_glow_intensity: 0.3,
427 key_press_time: 0.0,
428 channel_textures,
429 cubemap,
430 use_background_as_channel0: false,
431 background_channel_texture: None,
432 background_channel0_blend_mode,
433 background_color: [0.0, 0.0, 0.0, 0.0], progress_data: [0.0, 0.0, 0.0, 0.0],
435 command_data: [0.0, 0.0, 0.0, 0.0],
436 focused_pane: [0.0, 0.0, width as f32, height as f32],
437 scroll_data: [0.0, 0.0, 0.0, 0.0],
438 content_inset_right: 0.0,
439 custom_controls,
440 custom_uniform_values,
441 })
442 }
443
444 pub fn intermediate_texture_view(&self) -> &TextureView {
446 &self.intermediate_texture_view
447 }
448
449 pub fn render(
459 &mut self,
460 device: &Device,
461 queue: &Queue,
462 output_view: &TextureView,
463 apply_opacity: bool,
464 ) -> Result<()> {
465 self.render_with_clear_color(
466 device,
467 queue,
468 output_view,
469 apply_opacity,
470 Color::TRANSPARENT,
471 )
472 }
473
474 pub fn render_with_clear_color(
477 &mut self,
478 device: &Device,
479 queue: &Queue,
480 output_view: &TextureView,
481 apply_opacity: bool,
482 clear_color: Color,
483 ) -> Result<()> {
484 let now = Instant::now();
485
486 let time = if self.animation_enabled {
488 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
489 } else {
490 0.0
491 };
492
493 let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
495 self.last_frame_time = now;
496
497 self.frame_time_accumulator += time_delta;
499 self.frames_in_second += 1;
500 if self.frame_time_accumulator >= 1.0 {
501 self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
502 self.frame_time_accumulator = 0.0;
503 self.frames_in_second = 0;
504 }
505
506 self.frame_count = self.frame_count.wrapping_add(1);
507
508 let uniforms = self.build_uniforms(time, time_delta, apply_opacity);
510 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
511 let custom_uniforms =
512 crate::custom_shader_renderer::types::CustomShaderControlUniforms::from_controls(
513 &self.custom_controls,
514 &self.custom_uniform_values,
515 );
516 queue.write_buffer(
517 &self.custom_uniform_buffer,
518 0,
519 bytemuck::cast_slice(&[custom_uniforms]),
520 );
521
522 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
524 label: Some("Custom Shader Encoder"),
525 });
526
527 {
528 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
529 label: Some("Custom Shader Render Pass"),
530 color_attachments: &[Some(RenderPassColorAttachment {
531 view: output_view,
532 resolve_target: None,
533 ops: Operations {
534 load: LoadOp::Clear(clear_color),
535 store: StoreOp::Store,
536 },
537 depth_slice: None,
538 })],
539 depth_stencil_attachment: None,
540 timestamp_writes: None,
541 occlusion_query_set: None,
542 multiview_mask: None,
543 });
544
545 render_pass.set_pipeline(&self.pipeline);
551 render_pass.set_bind_group(0, &self.bind_group, &[]);
552 render_pass.draw(0..4, 0..1);
553 }
554
555 queue.submit(std::iter::once(encoder.finish()));
556 Ok(())
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use std::time::Duration;
564
565 #[test]
566 fn enabling_animation_when_already_enabled_preserves_start_time() {
567 let start_time = Instant::now() - Duration::from_secs(5);
568 let now = Instant::now();
569
570 assert_eq!(
571 animation_start_after_enabled_update(true, true, start_time, now),
572 start_time
573 );
574 }
575
576 #[test]
577 fn enabling_animation_from_disabled_starts_at_now() {
578 let start_time = Instant::now() - Duration::from_secs(5);
579 let now = Instant::now();
580
581 assert_eq!(
582 animation_start_after_enabled_update(false, true, start_time, now),
583 now
584 );
585 }
586
587 #[test]
588 fn debug_shader_wgsl_filename_matches_new_renderer_output_path() {
589 assert_eq!(
590 debug_shader_wgsl_filename("matrix"),
591 "/tmp/par_term_matrix_shader.wgsl"
592 );
593 }
594
595 #[test]
596 fn write_debug_shader_wgsl_refreshes_existing_output() {
597 let shader_name = format!("par_term_test_{}", std::process::id());
598 let path = debug_shader_wgsl_filename(&shader_name);
599 let _ = std::fs::remove_file(&path);
600
601 write_debug_shader_wgsl(&shader_name, "first");
602 write_debug_shader_wgsl(&shader_name, "second");
603
604 assert_eq!(
605 std::fs::read_to_string(&path).expect("read debug wgsl"),
606 "second"
607 );
608 std::fs::remove_file(&path).expect("remove debug wgsl");
609 }
610}