1use anyhow::{Context, Result};
16use par_term_emu_core_rust::cursor::CursorStyle;
17use std::path::Path;
18use std::time::Instant;
19use wgpu::*;
20
21pub mod textures;
22pub mod transpiler;
23pub mod types;
24
25use textures::{ChannelTexture, load_channel_textures};
26use transpiler::{transpile_glsl_to_wgsl, transpile_glsl_to_wgsl_source};
27use types::CustomShaderUniforms;
28
29pub struct CustomShaderRenderer {
31 pub(crate) pipeline: RenderPipeline,
33 pub(crate) bind_group: BindGroup,
35 pub(crate) uniform_buffer: Buffer,
37 pub(crate) intermediate_texture: Texture,
39 pub(crate) intermediate_texture_view: TextureView,
41 pub(crate) start_time: Instant,
43 pub(crate) animation_enabled: bool,
45 pub(crate) animation_speed: f32,
47 pub(crate) texture_width: u32,
49 pub(crate) texture_height: u32,
50 pub(crate) surface_format: TextureFormat,
52 pub(crate) bind_group_layout: BindGroupLayout,
54 pub(crate) sampler: Sampler,
56 pub(crate) window_opacity: f32,
58 pub(crate) text_opacity: f32,
60 pub(crate) full_content_mode: bool,
62 pub(crate) brightness: f32,
64 pub(crate) frame_count: u32,
66 pub(crate) last_frame_time: Instant,
68 pub(crate) mouse_position: [f32; 2],
70 pub(crate) mouse_click_position: [f32; 2],
72 pub(crate) mouse_button_down: bool,
74 pub(crate) frame_time_accumulator: f32,
76 pub(crate) frames_in_second: u32,
78 pub(crate) current_frame_rate: f32,
80
81 pub(crate) current_cursor_pos: (usize, usize),
84 pub(crate) previous_cursor_pos: (usize, usize),
86 pub(crate) current_cursor_color: [f32; 4],
88 pub(crate) previous_cursor_color: [f32; 4],
90 pub(crate) current_cursor_opacity: f32,
92 pub(crate) previous_cursor_opacity: f32,
94 pub(crate) cursor_change_time: f32,
96 pub(crate) current_cursor_style: CursorStyle,
98 pub(crate) previous_cursor_style: CursorStyle,
100 pub(crate) cursor_cell_width: f32,
102 pub(crate) cursor_cell_height: f32,
104 pub(crate) cursor_window_padding: f32,
106
107 pub(crate) cursor_shader_color: [f32; 4],
110 pub(crate) cursor_trail_duration: f32,
112 pub(crate) cursor_glow_radius: f32,
114 pub(crate) cursor_glow_intensity: f32,
116
117 pub(crate) channel_textures: [ChannelTexture; 4],
120}
121
122impl CustomShaderRenderer {
123 #[allow(clippy::too_many_arguments)]
139 pub fn new(
140 device: &Device,
141 queue: &Queue,
142 surface_format: TextureFormat,
143 shader_path: &Path,
144 width: u32,
145 height: u32,
146 animation_enabled: bool,
147 animation_speed: f32,
148 window_opacity: f32,
149 text_opacity: f32,
150 full_content_mode: bool,
151 channel_paths: &[Option<std::path::PathBuf>; 4],
152 ) -> Result<Self> {
153 let glsl_source = std::fs::read_to_string(shader_path)
155 .with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
156
157 let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
159
160 log::info!(
161 "Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
162 shader_path.display(),
163 glsl_source.len(),
164 wgsl_source.len()
165 );
166 log::debug!("Generated WGSL:\n{}", wgsl_source);
167
168 let module = naga::front::wgsl::parse_str(&wgsl_source)
171 .context("Custom shader WGSL parse failed")?;
172 let _info = naga::valid::Validator::new(
173 naga::valid::ValidationFlags::all(),
174 naga::valid::Capabilities::empty(),
175 )
176 .validate(&module)
177 .context("Custom shader WGSL validation failed")?;
178
179 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
180 label: Some("Custom Shader Module"),
181 source: ShaderSource::Wgsl(wgsl_source.clone().into()),
182 });
183
184 let (intermediate_texture, intermediate_texture_view) =
186 Self::create_intermediate_texture(device, surface_format, width, height);
187
188 let sampler = device.create_sampler(&SamplerDescriptor {
190 label: Some("Custom Shader Sampler"),
191 address_mode_u: AddressMode::ClampToEdge,
192 address_mode_v: AddressMode::ClampToEdge,
193 address_mode_w: AddressMode::ClampToEdge,
194 mag_filter: FilterMode::Linear,
195 min_filter: FilterMode::Linear,
196 mipmap_filter: FilterMode::Linear,
197 ..Default::default()
198 });
199
200 let channel_textures = load_channel_textures(device, queue, channel_paths);
202
203 let uniform_buffer = device.create_buffer(&BufferDescriptor {
205 label: Some("Custom Shader Uniforms"),
206 size: std::mem::size_of::<CustomShaderUniforms>() as u64,
207 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
208 mapped_at_creation: false,
209 });
210
211 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
224 label: Some("Custom Shader Bind Group Layout"),
225 entries: &[
226 BindGroupLayoutEntry {
228 binding: 0,
229 visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
230 ty: BindingType::Buffer {
231 ty: BufferBindingType::Uniform,
232 has_dynamic_offset: false,
233 min_binding_size: None,
234 },
235 count: None,
236 },
237 BindGroupLayoutEntry {
239 binding: 1,
240 visibility: ShaderStages::FRAGMENT,
241 ty: BindingType::Texture {
242 sample_type: TextureSampleType::Float { filterable: true },
243 view_dimension: TextureViewDimension::D2,
244 multisampled: false,
245 },
246 count: None,
247 },
248 BindGroupLayoutEntry {
250 binding: 2,
251 visibility: ShaderStages::FRAGMENT,
252 ty: BindingType::Sampler(SamplerBindingType::Filtering),
253 count: None,
254 },
255 BindGroupLayoutEntry {
257 binding: 3,
258 visibility: ShaderStages::FRAGMENT,
259 ty: BindingType::Texture {
260 sample_type: TextureSampleType::Float { filterable: true },
261 view_dimension: TextureViewDimension::D2,
262 multisampled: false,
263 },
264 count: None,
265 },
266 BindGroupLayoutEntry {
268 binding: 4,
269 visibility: ShaderStages::FRAGMENT,
270 ty: BindingType::Sampler(SamplerBindingType::Filtering),
271 count: None,
272 },
273 BindGroupLayoutEntry {
275 binding: 5,
276 visibility: ShaderStages::FRAGMENT,
277 ty: BindingType::Texture {
278 sample_type: TextureSampleType::Float { filterable: true },
279 view_dimension: TextureViewDimension::D2,
280 multisampled: false,
281 },
282 count: None,
283 },
284 BindGroupLayoutEntry {
286 binding: 6,
287 visibility: ShaderStages::FRAGMENT,
288 ty: BindingType::Sampler(SamplerBindingType::Filtering),
289 count: None,
290 },
291 BindGroupLayoutEntry {
293 binding: 7,
294 visibility: ShaderStages::FRAGMENT,
295 ty: BindingType::Texture {
296 sample_type: TextureSampleType::Float { filterable: true },
297 view_dimension: TextureViewDimension::D2,
298 multisampled: false,
299 },
300 count: None,
301 },
302 BindGroupLayoutEntry {
304 binding: 8,
305 visibility: ShaderStages::FRAGMENT,
306 ty: BindingType::Sampler(SamplerBindingType::Filtering),
307 count: None,
308 },
309 BindGroupLayoutEntry {
311 binding: 9,
312 visibility: ShaderStages::FRAGMENT,
313 ty: BindingType::Texture {
314 sample_type: TextureSampleType::Float { filterable: true },
315 view_dimension: TextureViewDimension::D2,
316 multisampled: false,
317 },
318 count: None,
319 },
320 BindGroupLayoutEntry {
322 binding: 10,
323 visibility: ShaderStages::FRAGMENT,
324 ty: BindingType::Sampler(SamplerBindingType::Filtering),
325 count: None,
326 },
327 ],
328 });
329
330 let bind_group = device.create_bind_group(&BindGroupDescriptor {
332 label: Some("Custom Shader Bind Group"),
333 layout: &bind_group_layout,
334 entries: &[
335 BindGroupEntry {
336 binding: 0,
337 resource: uniform_buffer.as_entire_binding(),
338 },
339 BindGroupEntry {
341 binding: 1,
342 resource: BindingResource::TextureView(&intermediate_texture_view),
343 },
344 BindGroupEntry {
345 binding: 2,
346 resource: BindingResource::Sampler(&sampler),
347 },
348 BindGroupEntry {
350 binding: 3,
351 resource: BindingResource::TextureView(&channel_textures[0].view),
352 },
353 BindGroupEntry {
354 binding: 4,
355 resource: BindingResource::Sampler(&channel_textures[0].sampler),
356 },
357 BindGroupEntry {
359 binding: 5,
360 resource: BindingResource::TextureView(&channel_textures[1].view),
361 },
362 BindGroupEntry {
363 binding: 6,
364 resource: BindingResource::Sampler(&channel_textures[1].sampler),
365 },
366 BindGroupEntry {
368 binding: 7,
369 resource: BindingResource::TextureView(&channel_textures[2].view),
370 },
371 BindGroupEntry {
372 binding: 8,
373 resource: BindingResource::Sampler(&channel_textures[2].sampler),
374 },
375 BindGroupEntry {
377 binding: 9,
378 resource: BindingResource::TextureView(&channel_textures[3].view),
379 },
380 BindGroupEntry {
381 binding: 10,
382 resource: BindingResource::Sampler(&channel_textures[3].sampler),
383 },
384 ],
385 });
386
387 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
389 label: Some("Custom Shader Pipeline Layout"),
390 bind_group_layouts: &[&bind_group_layout],
391 push_constant_ranges: &[],
392 });
393
394 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
396 label: Some("Custom Shader Pipeline"),
397 layout: Some(&pipeline_layout),
398 vertex: VertexState {
399 module: &shader_module,
400 entry_point: Some("vs_main"),
401 buffers: &[],
402 compilation_options: Default::default(),
403 },
404 fragment: Some(FragmentState {
405 module: &shader_module,
406 entry_point: Some("fs_main"),
407 targets: &[Some(ColorTargetState {
408 format: surface_format,
409 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
410 write_mask: ColorWrites::ALL,
411 })],
412 compilation_options: Default::default(),
413 }),
414 primitive: PrimitiveState {
415 topology: PrimitiveTopology::TriangleStrip,
416 ..Default::default()
417 },
418 depth_stencil: None,
419 multisample: MultisampleState::default(),
420 multiview: None,
421 cache: None,
422 });
423
424 let now = Instant::now();
425 Ok(Self {
426 pipeline,
427 bind_group,
428 uniform_buffer,
429 intermediate_texture,
430 intermediate_texture_view,
431 start_time: now,
432 animation_enabled,
433 animation_speed,
434 texture_width: width,
435 texture_height: height,
436 surface_format,
437 bind_group_layout,
438 sampler,
439 window_opacity,
440 text_opacity,
441 full_content_mode,
442 brightness: 1.0,
443 frame_count: 0,
444 last_frame_time: now,
445 mouse_position: [0.0, 0.0],
446 mouse_click_position: [0.0, 0.0],
447 mouse_button_down: false,
448 frame_time_accumulator: 0.0,
449 frames_in_second: 0,
450 current_frame_rate: 60.0, current_cursor_pos: (0, 0),
453 previous_cursor_pos: (0, 0),
454 current_cursor_color: [1.0, 1.0, 1.0, 1.0], previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
456 current_cursor_opacity: 1.0,
457 previous_cursor_opacity: 1.0,
458 cursor_change_time: 0.0,
459 current_cursor_style: CursorStyle::SteadyBlock,
460 previous_cursor_style: CursorStyle::SteadyBlock,
461 cursor_cell_width: 10.0, cursor_cell_height: 20.0,
463 cursor_window_padding: 0.0,
464 cursor_shader_color: [1.0, 1.0, 1.0, 1.0], cursor_trail_duration: 0.5,
467 cursor_glow_radius: 80.0,
468 cursor_glow_intensity: 0.3,
469 channel_textures,
471 })
472 }
473
474 fn create_intermediate_texture(
476 device: &Device,
477 format: TextureFormat,
478 width: u32,
479 height: u32,
480 ) -> (Texture, TextureView) {
481 let texture = device.create_texture(&TextureDescriptor {
482 label: Some("Custom Shader Intermediate Texture"),
483 size: Extent3d {
484 width: width.max(1),
485 height: height.max(1),
486 depth_or_array_layers: 1,
487 },
488 mip_level_count: 1,
489 sample_count: 1,
490 dimension: TextureDimension::D2,
491 format,
492 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
493 view_formats: &[],
494 });
495
496 let view = texture.create_view(&TextureViewDescriptor::default());
497 (texture, view)
498 }
499
500 pub fn intermediate_texture_view(&self) -> &TextureView {
502 &self.intermediate_texture_view
503 }
504
505 pub fn resize(&mut self, device: &Device, width: u32, height: u32) {
507 if width == self.texture_width && height == self.texture_height {
508 return;
509 }
510
511 self.texture_width = width;
512 self.texture_height = height;
513
514 let (texture, view) =
516 Self::create_intermediate_texture(device, self.surface_format, width, height);
517 self.intermediate_texture = texture;
518 self.intermediate_texture_view = view;
519
520 self.bind_group = device.create_bind_group(&BindGroupDescriptor {
522 label: Some("Custom Shader Bind Group"),
523 layout: &self.bind_group_layout,
524 entries: &[
525 BindGroupEntry {
526 binding: 0,
527 resource: self.uniform_buffer.as_entire_binding(),
528 },
529 BindGroupEntry {
531 binding: 1,
532 resource: BindingResource::TextureView(&self.intermediate_texture_view),
533 },
534 BindGroupEntry {
535 binding: 2,
536 resource: BindingResource::Sampler(&self.sampler),
537 },
538 BindGroupEntry {
540 binding: 3,
541 resource: BindingResource::TextureView(&self.channel_textures[0].view),
542 },
543 BindGroupEntry {
544 binding: 4,
545 resource: BindingResource::Sampler(&self.channel_textures[0].sampler),
546 },
547 BindGroupEntry {
549 binding: 5,
550 resource: BindingResource::TextureView(&self.channel_textures[1].view),
551 },
552 BindGroupEntry {
553 binding: 6,
554 resource: BindingResource::Sampler(&self.channel_textures[1].sampler),
555 },
556 BindGroupEntry {
558 binding: 7,
559 resource: BindingResource::TextureView(&self.channel_textures[2].view),
560 },
561 BindGroupEntry {
562 binding: 8,
563 resource: BindingResource::Sampler(&self.channel_textures[2].sampler),
564 },
565 BindGroupEntry {
567 binding: 9,
568 resource: BindingResource::TextureView(&self.channel_textures[3].view),
569 },
570 BindGroupEntry {
571 binding: 10,
572 resource: BindingResource::Sampler(&self.channel_textures[3].sampler),
573 },
574 ],
575 });
576 }
577
578 pub fn render(
583 &mut self,
584 device: &Device,
585 queue: &Queue,
586 output_view: &TextureView,
587 ) -> Result<()> {
588 let now = Instant::now();
589
590 let time = if self.animation_enabled {
592 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
593 } else {
594 0.0
595 };
596
597 let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
599 self.last_frame_time = now;
600
601 self.frame_time_accumulator += time_delta;
603 self.frames_in_second += 1;
604 if self.frame_time_accumulator >= 1.0 {
605 self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
606 self.frame_time_accumulator = 0.0;
607 self.frames_in_second = 0;
608 }
609
610 self.frame_count = self.frame_count.wrapping_add(1);
612
613 let height = self.texture_height as f32;
617 let mouse_y_flipped = height - self.mouse_position[1];
618 let click_y_flipped = height - self.mouse_click_position[1];
619
620 let mouse = if self.mouse_button_down {
621 [
623 self.mouse_position[0],
624 mouse_y_flipped,
625 self.mouse_click_position[0],
626 click_y_flipped,
627 ]
628 } else {
629 [
631 self.mouse_position[0],
632 mouse_y_flipped,
633 -self.mouse_click_position[0].abs(),
634 -click_y_flipped.abs(),
635 ]
636 };
637
638 let date = {
641 use std::time::{SystemTime, UNIX_EPOCH};
642 let now_sys = SystemTime::now();
643 let since_epoch = now_sys.duration_since(UNIX_EPOCH).unwrap_or_default();
644 let secs = since_epoch.as_secs();
645
646 let days_since_epoch = secs / 86400;
649 let secs_today = (secs % 86400) as f32;
650
651 let mut year = 1970i32;
654 let mut remaining_days = days_since_epoch as i32;
655
656 loop {
657 let days_in_year = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
658 366
659 } else {
660 365
661 };
662 if remaining_days < days_in_year {
663 break;
664 }
665 remaining_days -= days_in_year;
666 year += 1;
667 }
668
669 let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
670 let days_in_months: [i32; 12] = if is_leap {
671 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
672 } else {
673 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
674 };
675
676 let mut month = 0i32;
677 for (i, &days) in days_in_months.iter().enumerate() {
678 if remaining_days < days {
679 month = i as i32;
680 break;
681 }
682 remaining_days -= days;
683 }
684
685 let day = remaining_days + 1; [year as f32, month as f32, day as f32, secs_today]
688 };
689
690 let (curr_x, curr_y) =
692 self.cursor_to_pixels(self.current_cursor_pos.0, self.current_cursor_pos.1);
693 let (prev_x, prev_y) =
694 self.cursor_to_pixels(self.previous_cursor_pos.0, self.previous_cursor_pos.1);
695
696 if self.frame_count.is_multiple_of(60) {
698 log::debug!(
699 "CURSOR_SHADER: pos=({},{}) -> pixels=({:.1},{:.1}), cell=({:.1}x{:.1}), padding={:.1}, resolution={}x{}",
700 self.current_cursor_pos.0,
701 self.current_cursor_pos.1,
702 curr_x,
703 curr_y,
704 self.cursor_cell_width,
705 self.cursor_cell_height,
706 self.cursor_window_padding,
707 self.texture_width,
708 self.texture_height
709 );
710 }
711
712 let uniforms = CustomShaderUniforms {
714 resolution: [self.texture_width as f32, self.texture_height as f32],
715 time,
716 time_delta,
717 mouse,
718 date,
719 opacity: self.window_opacity,
720 text_opacity: self.text_opacity,
721 full_content_mode: if self.full_content_mode { 1.0 } else { 0.0 },
722 frame: self.frame_count as f32,
723 frame_rate: self.current_frame_rate,
724 resolution_z: 1.0, brightness: self.brightness,
726 _pad1: 0.0,
727 current_cursor: [
733 curr_x,
734 curr_y,
735 self.cursor_width_for_style(self.current_cursor_style),
736 self.cursor_height_for_style(self.current_cursor_style),
737 ],
738 previous_cursor: [
739 prev_x,
740 prev_y,
741 self.cursor_width_for_style(self.previous_cursor_style),
742 self.cursor_height_for_style(self.previous_cursor_style),
743 ],
744 current_cursor_color: [
745 self.current_cursor_color[0],
746 self.current_cursor_color[1],
747 self.current_cursor_color[2],
748 self.current_cursor_color[3] * self.current_cursor_opacity,
749 ],
750 previous_cursor_color: [
751 self.previous_cursor_color[0],
752 self.previous_cursor_color[1],
753 self.previous_cursor_color[2],
754 self.previous_cursor_color[3] * self.previous_cursor_opacity,
755 ],
756 cursor_change_time: self.cursor_change_time,
757 cursor_trail_duration: self.cursor_trail_duration,
759 cursor_glow_radius: self.cursor_glow_radius,
760 cursor_glow_intensity: self.cursor_glow_intensity,
761 cursor_shader_color: self.cursor_shader_color,
762 channel0_resolution: [
764 self.texture_width as f32,
765 self.texture_height as f32,
766 1.0,
767 0.0,
768 ],
769 channel1_resolution: self.channel_textures[0].resolution(),
770 channel2_resolution: self.channel_textures[1].resolution(),
771 channel3_resolution: self.channel_textures[2].resolution(),
772 channel4_resolution: self.channel_textures[3].resolution(),
773 };
774
775 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
776
777 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
779 label: Some("Custom Shader Encoder"),
780 });
781
782 {
784 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
785 label: Some("Custom Shader Render Pass"),
786 color_attachments: &[Some(RenderPassColorAttachment {
787 view: output_view,
788 resolve_target: None,
789 ops: Operations {
790 load: LoadOp::Clear(Color::TRANSPARENT),
792 store: StoreOp::Store,
793 },
794 depth_slice: None,
795 })],
796 depth_stencil_attachment: None,
797 timestamp_writes: None,
798 occlusion_query_set: None,
799 });
800
801 render_pass.set_pipeline(&self.pipeline);
802 render_pass.set_bind_group(0, &self.bind_group, &[]);
803 render_pass.draw(0..4, 0..1);
804 }
805
806 queue.submit(std::iter::once(encoder.finish()));
807
808 Ok(())
809 }
810
811 #[allow(dead_code)]
813 pub fn animation_enabled(&self) -> bool {
814 self.animation_enabled
815 }
816
817 #[allow(dead_code)]
819 pub fn set_animation_enabled(&mut self, enabled: bool) {
820 self.animation_enabled = enabled;
821 if enabled {
822 self.start_time = Instant::now();
824 }
825 }
826
827 pub fn set_animation_speed(&mut self, speed: f32) {
829 self.animation_speed = speed.max(0.0);
830 }
831
832 pub fn set_opacity(&mut self, opacity: f32) {
834 self.window_opacity = opacity.clamp(0.0, 1.0);
835 }
836
837 pub fn set_brightness(&mut self, brightness: f32) {
839 self.brightness = brightness.clamp(0.05, 1.0);
840 }
841
842 pub fn set_full_content_mode(&mut self, enabled: bool) {
844 self.full_content_mode = enabled;
845 }
846
847 #[allow(dead_code)]
849 pub fn full_content_mode(&self) -> bool {
850 self.full_content_mode
851 }
852
853 pub fn set_mouse_position(&mut self, x: f32, y: f32) {
859 self.mouse_position = [x, y];
860 }
861
862 pub fn set_mouse_button(&mut self, pressed: bool, x: f32, y: f32) {
871 self.mouse_button_down = pressed;
872 if pressed {
873 self.mouse_click_position = [x, y];
875 }
876 }
877
878 pub fn update_cursor(
892 &mut self,
893 col: usize,
894 row: usize,
895 opacity: f32,
896 cursor_color: [f32; 4],
897 style: CursorStyle,
898 ) {
899 let new_pos = (col, row);
900 let style_changed = style != self.current_cursor_style;
901 let pos_changed = new_pos != self.current_cursor_pos;
902
903 if pos_changed || style_changed {
904 self.previous_cursor_pos = self.current_cursor_pos;
906 self.previous_cursor_opacity = self.current_cursor_opacity;
907 self.previous_cursor_color = self.current_cursor_color;
908 self.previous_cursor_style = self.current_cursor_style;
909 self.current_cursor_pos = new_pos;
910 self.current_cursor_style = style;
911
912 self.cursor_change_time = if self.animation_enabled {
914 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
915 } else {
916 0.0
917 };
918
919 if pos_changed {
920 log::trace!(
921 "Cursor moved: ({}, {}) -> ({}, {}), change_time={:.3}",
922 self.previous_cursor_pos.0,
923 self.previous_cursor_pos.1,
924 col,
925 row,
926 self.cursor_change_time
927 );
928 }
929 }
930 self.current_cursor_opacity = opacity;
931 self.current_cursor_color = cursor_color;
932 }
933
934 pub fn update_cell_dimensions(&mut self, cell_width: f32, cell_height: f32, padding: f32) {
941 self.cursor_cell_width = cell_width;
942 self.cursor_cell_height = cell_height;
943 self.cursor_window_padding = padding;
944 }
945
946 fn cursor_to_pixels(&self, col: usize, row: usize) -> (f32, f32) {
950 let x = self.cursor_window_padding + (col as f32 * self.cursor_cell_width);
951 let y = self.cursor_window_padding + (row as f32 * self.cursor_cell_height);
952 (x, y)
953 }
954
955 fn cursor_width_for_style(&self, style: CursorStyle) -> f32 {
957 match style {
958 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => self.cursor_cell_width,
960 CursorStyle::SteadyBar | CursorStyle::BlinkingBar => 2.0,
962 CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => self.cursor_cell_width,
964 }
965 }
966
967 fn cursor_height_for_style(&self, style: CursorStyle) -> f32 {
969 match style {
970 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => self.cursor_cell_height,
972 CursorStyle::SteadyBar | CursorStyle::BlinkingBar => self.cursor_cell_height,
974 CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => 2.0,
976 }
977 }
978
979 pub fn cursor_needs_animation(&self) -> bool {
984 if self.animation_enabled {
985 let current_time =
986 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0);
987 (current_time - self.cursor_change_time) < 1.0
989 } else {
990 false
991 }
992 }
993
994 #[allow(dead_code)]
1005 pub fn update_channel_texture(
1006 &mut self,
1007 device: &Device,
1008 queue: &Queue,
1009 channel: u8,
1010 path: Option<&std::path::Path>,
1011 ) -> Result<()> {
1012 if !(1..=4).contains(&channel) {
1013 anyhow::bail!("Invalid channel index: {} (must be 1-4)", channel);
1014 }
1015
1016 let index = (channel - 1) as usize;
1017
1018 let new_texture = match path {
1020 Some(p) => ChannelTexture::from_file(device, queue, p)?,
1021 None => ChannelTexture::placeholder(device, queue),
1022 };
1023
1024 self.channel_textures[index] = new_texture;
1026
1027 self.bind_group = device.create_bind_group(&BindGroupDescriptor {
1029 label: Some("Custom Shader Bind Group"),
1030 layout: &self.bind_group_layout,
1031 entries: &[
1032 BindGroupEntry {
1033 binding: 0,
1034 resource: self.uniform_buffer.as_entire_binding(),
1035 },
1036 BindGroupEntry {
1038 binding: 1,
1039 resource: BindingResource::TextureView(&self.intermediate_texture_view),
1040 },
1041 BindGroupEntry {
1042 binding: 2,
1043 resource: BindingResource::Sampler(&self.sampler),
1044 },
1045 BindGroupEntry {
1047 binding: 3,
1048 resource: BindingResource::TextureView(&self.channel_textures[0].view),
1049 },
1050 BindGroupEntry {
1051 binding: 4,
1052 resource: BindingResource::Sampler(&self.channel_textures[0].sampler),
1053 },
1054 BindGroupEntry {
1056 binding: 5,
1057 resource: BindingResource::TextureView(&self.channel_textures[1].view),
1058 },
1059 BindGroupEntry {
1060 binding: 6,
1061 resource: BindingResource::Sampler(&self.channel_textures[1].sampler),
1062 },
1063 BindGroupEntry {
1065 binding: 7,
1066 resource: BindingResource::TextureView(&self.channel_textures[2].view),
1067 },
1068 BindGroupEntry {
1069 binding: 8,
1070 resource: BindingResource::Sampler(&self.channel_textures[2].sampler),
1071 },
1072 BindGroupEntry {
1074 binding: 9,
1075 resource: BindingResource::TextureView(&self.channel_textures[3].view),
1076 },
1077 BindGroupEntry {
1078 binding: 10,
1079 resource: BindingResource::Sampler(&self.channel_textures[3].sampler),
1080 },
1081 ],
1082 });
1083
1084 log::info!(
1085 "Updated iChannel{} texture: {}",
1086 channel,
1087 path.map(|p| p.display().to_string())
1088 .unwrap_or_else(|| "placeholder".to_string())
1089 );
1090
1091 Ok(())
1092 }
1093
1094 pub fn update_cursor_shader_config(
1102 &mut self,
1103 color: [u8; 3],
1104 trail_duration: f32,
1105 glow_radius: f32,
1106 glow_intensity: f32,
1107 ) {
1108 self.cursor_shader_color = [
1109 color[0] as f32 / 255.0,
1110 color[1] as f32 / 255.0,
1111 color[2] as f32 / 255.0,
1112 1.0,
1113 ];
1114 self.cursor_trail_duration = trail_duration.max(0.0);
1115 self.cursor_glow_radius = glow_radius.max(0.0);
1116 self.cursor_glow_intensity = glow_intensity.clamp(0.0, 1.0);
1117 }
1118
1119 pub fn reload_from_source(&mut self, device: &Device, source: &str, name: &str) -> Result<()> {
1132 let wgsl_source = transpile_glsl_to_wgsl_source(source, name)?;
1134
1135 log::info!(
1136 "Reloading custom shader from source ({} bytes GLSL -> {} bytes WGSL)",
1137 source.len(),
1138 wgsl_source.len()
1139 );
1140 log::debug!("Generated WGSL:\n{}", wgsl_source);
1141
1142 let module = naga::front::wgsl::parse_str(&wgsl_source)
1144 .context("Custom shader WGSL parse failed")?;
1145 let _info = naga::valid::Validator::new(
1146 naga::valid::ValidationFlags::all(),
1147 naga::valid::Capabilities::empty(),
1148 )
1149 .validate(&module)
1150 .context("Custom shader WGSL validation failed")?;
1151
1152 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
1154 label: Some("Custom Shader Module (reloaded)"),
1155 source: ShaderSource::Wgsl(wgsl_source.into()),
1156 });
1157
1158 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
1160 label: Some("Custom Shader Pipeline Layout (reloaded)"),
1161 bind_group_layouts: &[&self.bind_group_layout],
1162 push_constant_ranges: &[],
1163 });
1164
1165 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
1167 label: Some("Custom Shader Pipeline (reloaded)"),
1168 layout: Some(&pipeline_layout),
1169 vertex: VertexState {
1170 module: &shader_module,
1171 entry_point: Some("vs_main"),
1172 buffers: &[],
1173 compilation_options: Default::default(),
1174 },
1175 fragment: Some(FragmentState {
1176 module: &shader_module,
1177 entry_point: Some("fs_main"),
1178 targets: &[Some(ColorTargetState {
1179 format: self.surface_format,
1180 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1181 write_mask: ColorWrites::ALL,
1182 })],
1183 compilation_options: Default::default(),
1184 }),
1185 primitive: PrimitiveState {
1186 topology: PrimitiveTopology::TriangleStrip,
1187 ..Default::default()
1188 },
1189 depth_stencil: None,
1190 multisample: MultisampleState::default(),
1191 multiview: None,
1192 cache: None,
1193 });
1194
1195 self.pipeline = pipeline;
1197
1198 self.start_time = Instant::now();
1200
1201 log::info!("Custom shader reloaded successfully from source");
1202 Ok(())
1203 }
1204}