1use anyhow::{Context, Result};
31use par_term_emu_core_rust::cursor::CursorStyle;
32use std::path::Path;
33use std::time::Instant;
34use wgpu::*;
35
36mod cubemap;
37mod cursor;
38mod hot_reload;
39pub mod pipeline;
40pub mod textures;
41pub mod transpiler;
42pub mod types;
43mod uniforms;
44
45use cubemap::CubemapTexture;
46use pipeline::{create_bind_group, create_bind_group_layout, create_render_pipeline};
47use textures::{ChannelTexture, load_channel_textures};
48use transpiler::transpile_glsl_to_wgsl;
49
50pub struct CustomShaderRenderer {
52 pub(crate) pipeline: RenderPipeline,
54 pub(crate) bind_group: BindGroup,
56 pub(crate) uniform_buffer: Buffer,
58 pub(crate) intermediate_texture: Texture,
60 pub(crate) intermediate_texture_view: TextureView,
62 pub(crate) start_time: Instant,
64 pub(crate) animation_enabled: bool,
66 pub(crate) animation_speed: f32,
68 pub(crate) texture_width: u32,
70 pub(crate) texture_height: u32,
71 pub(crate) surface_format: TextureFormat,
73 pub(crate) bind_group_layout: BindGroupLayout,
75 pub(crate) sampler: Sampler,
77 pub(crate) scale_factor: f32,
79 pub(crate) window_opacity: f32,
81 pub(crate) keep_text_opaque: bool,
83 pub(crate) full_content_mode: bool,
85 pub(crate) brightness: f32,
87 pub(crate) frame_count: u32,
89 pub(crate) last_frame_time: Instant,
91 pub(crate) mouse_position: [f32; 2],
93 pub(crate) mouse_click_position: [f32; 2],
95 pub(crate) mouse_button_down: bool,
97 pub(crate) frame_time_accumulator: f32,
99 pub(crate) frames_in_second: u32,
101 pub(crate) current_frame_rate: f32,
103
104 pub(crate) current_cursor_pos: (usize, usize),
107 pub(crate) previous_cursor_pos: (usize, usize),
109 pub(crate) current_cursor_color: [f32; 4],
111 pub(crate) previous_cursor_color: [f32; 4],
113 pub(crate) current_cursor_opacity: f32,
115 pub(crate) previous_cursor_opacity: f32,
117 pub(crate) cursor_change_time: f32,
119 pub(crate) current_cursor_style: CursorStyle,
121 pub(crate) previous_cursor_style: CursorStyle,
123 pub(crate) cursor_cell_width: f32,
125 pub(crate) cursor_cell_height: f32,
127 pub(crate) cursor_window_padding: f32,
129 pub(crate) cursor_content_offset_y: f32,
131 pub(crate) cursor_content_offset_x: f32,
133
134 pub(crate) cursor_shader_color: [f32; 4],
137 pub(crate) cursor_trail_duration: f32,
139 pub(crate) cursor_glow_radius: f32,
141 pub(crate) cursor_glow_intensity: f32,
143
144 pub(crate) key_press_time: f32,
147
148 pub(crate) channel_textures: [ChannelTexture; 4],
151
152 pub(crate) cubemap: CubemapTexture,
155
156 pub(crate) use_background_as_channel0: bool,
159 pub(crate) background_channel_texture: Option<ChannelTexture>,
162
163 pub(crate) background_color: [f32; 4],
168
169 pub(crate) progress_data: [f32; 4],
172
173 pub(crate) content_inset_right: f32,
177}
178
179pub struct CustomShaderRendererConfig<'a> {
181 pub surface_format: TextureFormat,
182 pub shader_path: &'a Path,
183 pub width: u32,
184 pub height: u32,
185 pub animation_enabled: bool,
186 pub animation_speed: f32,
187 pub window_opacity: f32,
188 pub full_content_mode: bool,
189 pub channel_paths: &'a [Option<std::path::PathBuf>; 4],
190 pub cubemap_path: Option<&'a Path>,
191}
192
193impl CustomShaderRenderer {
194 pub fn new(
196 device: &Device,
197 queue: &Queue,
198 config: CustomShaderRendererConfig<'_>,
199 ) -> Result<Self> {
200 let CustomShaderRendererConfig {
201 surface_format,
202 shader_path,
203 width,
204 height,
205 animation_enabled,
206 animation_speed,
207 window_opacity,
208 full_content_mode,
209 channel_paths,
210 cubemap_path,
211 } = config;
212 let glsl_source = std::fs::read_to_string(shader_path)
214 .with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
215
216 let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
218
219 log::info!(
220 "Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
221 shader_path.display(),
222 glsl_source.len(),
223 wgsl_source.len()
224 );
225 log::debug!("Generated WGSL:\n{}", wgsl_source);
226
227 let shader_name = shader_path
229 .file_stem()
230 .and_then(|s| s.to_str())
231 .unwrap_or("unknown");
232 let debug_filename = format!("/tmp/par_term_{}_shader.wgsl", shader_name);
233 if let Err(e) = std::fs::write(&debug_filename, &wgsl_source) {
234 log::warn!("Failed to write debug shader: {}", e);
235 } else {
236 log::info!("Wrote debug shader to {}", debug_filename);
237 }
238
239 let module = naga::front::wgsl::parse_str(&wgsl_source)
241 .context("Custom shader WGSL parse failed")?;
242 let _info = naga::valid::Validator::new(
243 naga::valid::ValidationFlags::all(),
244 naga::valid::Capabilities::empty(),
245 )
246 .validate(&module)
247 .context("Custom shader WGSL validation failed")?;
248
249 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
250 label: Some("Custom Shader Module"),
251 source: ShaderSource::Wgsl(wgsl_source.clone().into()),
252 });
253
254 let (intermediate_texture, intermediate_texture_view) =
256 Self::create_intermediate_texture(device, surface_format, width, height);
257
258 let sampler = device.create_sampler(&SamplerDescriptor {
261 label: Some("Custom Shader Sampler"),
262 address_mode_u: AddressMode::ClampToEdge,
263 address_mode_v: AddressMode::ClampToEdge,
264 address_mode_w: AddressMode::ClampToEdge,
265 mag_filter: FilterMode::Nearest,
266 min_filter: FilterMode::Nearest,
267 mipmap_filter: FilterMode::Nearest,
268 ..Default::default()
269 });
270
271 let channel_textures = load_channel_textures(device, queue, channel_paths);
273
274 let cubemap = match cubemap_path {
276 Some(path) => match CubemapTexture::from_prefix(device, queue, path) {
277 Ok(cm) => cm,
278 Err(e) => {
279 log::error!("Failed to load cubemap '{}': {}", path.display(), e);
280 CubemapTexture::placeholder(device, queue)
281 }
282 },
283 None => CubemapTexture::placeholder(device, queue),
284 };
285
286 let uniform_buffer = Self::create_uniform_buffer(device);
288
289 let bind_group_layout = create_bind_group_layout(device);
291 let bind_group = create_bind_group(
292 device,
293 &bind_group_layout,
294 &uniform_buffer,
295 &intermediate_texture_view,
296 &sampler,
297 &channel_textures,
298 &cubemap,
299 );
300
301 let pipeline = create_render_pipeline(
303 device,
304 &shader_module,
305 &bind_group_layout,
306 surface_format,
307 Some("Custom Shader Pipeline"),
308 );
309
310 let now = Instant::now();
311 Ok(Self {
312 pipeline,
313 bind_group,
314 uniform_buffer,
315 intermediate_texture,
316 intermediate_texture_view,
317 start_time: now,
318 animation_enabled,
319 animation_speed,
320 texture_width: width,
321 texture_height: height,
322 surface_format,
323 bind_group_layout,
324 sampler,
325 window_opacity,
326 keep_text_opaque: false,
327 scale_factor: 1.0,
328 full_content_mode,
329 brightness: 1.0,
330 frame_count: 0,
331 last_frame_time: now,
332 mouse_position: [0.0, 0.0],
333 mouse_click_position: [0.0, 0.0],
334 mouse_button_down: false,
335 frame_time_accumulator: 0.0,
336 frames_in_second: 0,
337 current_frame_rate: 60.0,
338 current_cursor_pos: (0, 0),
339 previous_cursor_pos: (0, 0),
340 current_cursor_color: [1.0, 1.0, 1.0, 1.0],
341 previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
342 current_cursor_opacity: 1.0,
343 previous_cursor_opacity: 1.0,
344 cursor_change_time: 0.0,
345 current_cursor_style: CursorStyle::SteadyBlock,
346 previous_cursor_style: CursorStyle::SteadyBlock,
347 cursor_cell_width: 10.0,
348 cursor_cell_height: 20.0,
349 cursor_window_padding: 0.0,
350 cursor_content_offset_y: 0.0,
351 cursor_content_offset_x: 0.0,
352 cursor_shader_color: [1.0, 1.0, 1.0, 1.0],
353 cursor_trail_duration: 0.5,
354 cursor_glow_radius: 80.0,
355 cursor_glow_intensity: 0.3,
356 key_press_time: 0.0,
357 channel_textures,
358 cubemap,
359 use_background_as_channel0: false,
360 background_channel_texture: None,
361 background_color: [0.0, 0.0, 0.0, 0.0], progress_data: [0.0, 0.0, 0.0, 0.0],
363 content_inset_right: 0.0,
364 })
365 }
366
367 pub fn intermediate_texture_view(&self) -> &TextureView {
369 &self.intermediate_texture_view
370 }
371
372 pub fn render(
382 &mut self,
383 device: &Device,
384 queue: &Queue,
385 output_view: &TextureView,
386 apply_opacity: bool,
387 ) -> Result<()> {
388 self.render_with_clear_color(
389 device,
390 queue,
391 output_view,
392 apply_opacity,
393 Color::TRANSPARENT,
394 )
395 }
396
397 pub fn render_with_clear_color(
400 &mut self,
401 device: &Device,
402 queue: &Queue,
403 output_view: &TextureView,
404 apply_opacity: bool,
405 clear_color: Color,
406 ) -> Result<()> {
407 let now = Instant::now();
408
409 let time = if self.animation_enabled {
411 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
412 } else {
413 0.0
414 };
415
416 let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
418 self.last_frame_time = now;
419
420 self.frame_time_accumulator += time_delta;
422 self.frames_in_second += 1;
423 if self.frame_time_accumulator >= 1.0 {
424 self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
425 self.frame_time_accumulator = 0.0;
426 self.frames_in_second = 0;
427 }
428
429 self.frame_count = self.frame_count.wrapping_add(1);
430
431 let uniforms = self.build_uniforms(time, time_delta, apply_opacity);
433 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
434
435 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
437 label: Some("Custom Shader Encoder"),
438 });
439
440 {
441 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
442 label: Some("Custom Shader Render Pass"),
443 color_attachments: &[Some(RenderPassColorAttachment {
444 view: output_view,
445 resolve_target: None,
446 ops: Operations {
447 load: LoadOp::Clear(clear_color),
448 store: StoreOp::Store,
449 },
450 depth_slice: None,
451 })],
452 depth_stencil_attachment: None,
453 timestamp_writes: None,
454 occlusion_query_set: None,
455 });
456
457 render_pass.set_pipeline(&self.pipeline);
463 render_pass.set_bind_group(0, &self.bind_group, &[]);
464 render_pass.draw(0..4, 0..1);
465 }
466
467 queue.submit(std::iter::once(encoder.finish()));
468 Ok(())
469 }
470
471 pub fn animation_enabled(&self) -> bool {
473 self.animation_enabled
474 }
475
476 pub fn set_animation_enabled(&mut self, enabled: bool) {
478 self.animation_enabled = enabled;
479 if enabled {
480 self.start_time = Instant::now();
481 }
482 }
483
484 pub fn set_animation_speed(&mut self, speed: f32) {
486 self.animation_speed = speed.max(0.0);
487 }
488
489 pub fn set_opacity(&mut self, opacity: f32) {
491 self.window_opacity = opacity.clamp(0.0, 1.0);
492 }
493
494 pub fn set_brightness(&mut self, brightness: f32) {
496 self.brightness = brightness.clamp(0.05, 1.0);
497 }
498
499 pub fn set_full_content_mode(&mut self, enabled: bool) {
501 self.full_content_mode = enabled;
502 }
503
504 pub fn full_content_mode(&self) -> bool {
506 self.full_content_mode
507 }
508
509 pub fn set_keep_text_opaque(&mut self, keep_opaque: bool) {
512 self.keep_text_opaque = keep_opaque;
513 }
514
515 pub fn set_mouse_position(&mut self, x: f32, y: f32) {
517 self.mouse_position = [x, y];
518 }
519
520 pub fn set_mouse_button(&mut self, pressed: bool, x: f32, y: f32) {
522 self.mouse_button_down = pressed;
523 if pressed {
524 self.mouse_click_position = [x, y];
525 }
526 }
527
528 pub fn update_key_press(&mut self) {
533 self.key_press_time = if self.animation_enabled {
534 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
535 } else {
536 0.0
537 };
538 log::trace!("Key pressed at shader time={:.3}", self.key_press_time);
539 }
540
541 pub fn update_channel_texture(
543 &mut self,
544 device: &Device,
545 queue: &Queue,
546 channel: u8,
547 path: Option<&std::path::Path>,
548 ) -> Result<()> {
549 if !(1..=4).contains(&channel) {
550 anyhow::bail!("Invalid channel index: {} (must be 1-4)", channel);
551 }
552
553 let index = (channel - 1) as usize;
554
555 let new_texture = match path {
556 Some(p) => ChannelTexture::from_file(device, queue, p)?,
557 None => ChannelTexture::placeholder(device, queue),
558 };
559
560 self.channel_textures[index] = new_texture;
561
562 self.recreate_bind_group(device);
564
565 log::info!(
566 "Updated iChannel{} texture: {}",
567 channel,
568 path.map(|p| p.display().to_string())
569 .unwrap_or_else(|| "placeholder".to_string())
570 );
571
572 Ok(())
573 }
574
575 pub fn update_cubemap(
577 &mut self,
578 device: &Device,
579 queue: &Queue,
580 path: Option<&std::path::Path>,
581 ) -> Result<()> {
582 let new_cubemap = match path {
583 Some(p) => CubemapTexture::from_prefix(device, queue, p)?,
584 None => CubemapTexture::placeholder(device, queue),
585 };
586
587 self.cubemap = new_cubemap;
588
589 self.recreate_bind_group(device);
591
592 log::info!(
593 "Updated cubemap texture: {}",
594 path.map(|p| p.display().to_string())
595 .unwrap_or_else(|| "placeholder".to_string())
596 );
597
598 Ok(())
599 }
600
601 pub fn set_use_background_as_channel0(&mut self, use_background: bool) {
609 if self.use_background_as_channel0 != use_background {
610 self.use_background_as_channel0 = use_background;
611 log::info!("use_background_as_channel0 set to {}", use_background);
612 }
613 }
614
615 pub fn use_background_as_channel0(&self) -> bool {
617 self.use_background_as_channel0
618 }
619
620 pub fn set_background_texture(&mut self, device: &Device, texture: Option<ChannelTexture>) {
632 self.background_channel_texture = texture;
633
634 if self.use_background_as_channel0 {
637 self.recreate_bind_group(device);
638 }
639 }
640
641 pub fn set_background_color(&mut self, color: [f32; 3], active: bool) {
650 self.background_color = [color[0], color[1], color[2], if active { 1.0 } else { 0.0 }];
651 }
652
653 pub fn update_progress(&mut self, state: f32, percent: f32, is_active: f32, active_count: f32) {
661 self.progress_data = [state, percent, is_active, active_count];
662 }
663
664 pub fn update_use_background_as_channel0(&mut self, device: &Device, use_background: bool) {
668 if self.use_background_as_channel0 != use_background {
669 self.use_background_as_channel0 = use_background;
670 self.recreate_bind_group(device);
671 log::info!("use_background_as_channel0 toggled to {}", use_background);
672 }
673 }
674
675 pub fn set_content_inset_right(&mut self, inset: f32) {
680 self.content_inset_right = inset;
681 }
682}