1use anyhow::{Context, Result};
16use par_term_emu_core_rust::cursor::CursorStyle;
17use std::path::Path;
18use std::time::Instant;
19use wgpu::*;
20
21#[repr(C)]
28#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
29struct CustomShaderUniforms {
30 resolution: [f32; 2],
32 time: f32,
34 time_delta: f32,
36 mouse: [f32; 4],
40 date: [f32; 4],
43 opacity: f32,
45 text_opacity: f32,
47 full_content_mode: f32,
49 frame: f32,
51 frame_rate: f32,
53 resolution_z: f32,
55 _pad1: [f32; 2],
57
58 current_cursor: [f32; 4],
62 previous_cursor: [f32; 4],
64 current_cursor_color: [f32; 4],
66 previous_cursor_color: [f32; 4],
68 cursor_change_time: f32,
70
71 cursor_trail_duration: f32,
75 cursor_glow_radius: f32,
77 cursor_glow_intensity: f32,
79 cursor_shader_color: [f32; 4],
82}
83const _: () = assert!(
87 std::mem::size_of::<CustomShaderUniforms>() == 176,
88 "CustomShaderUniforms must be exactly 176 bytes for GPU compatibility"
89);
90
91pub struct CustomShaderRenderer {
93 pipeline: RenderPipeline,
95 bind_group: BindGroup,
97 uniform_buffer: Buffer,
99 intermediate_texture: Texture,
101 intermediate_texture_view: TextureView,
103 start_time: Instant,
105 animation_enabled: bool,
107 animation_speed: f32,
109 texture_width: u32,
111 texture_height: u32,
112 surface_format: TextureFormat,
114 bind_group_layout: BindGroupLayout,
116 sampler: Sampler,
118 window_opacity: f32,
120 text_opacity: f32,
122 full_content_mode: bool,
124 frame_count: u32,
126 last_frame_time: Instant,
128 mouse_position: [f32; 2],
130 mouse_click_position: [f32; 2],
132 mouse_button_down: bool,
134 frame_time_accumulator: f32,
136 frames_in_second: u32,
138 current_frame_rate: f32,
140
141 current_cursor_pos: (usize, usize),
144 previous_cursor_pos: (usize, usize),
146 current_cursor_color: [f32; 4],
148 previous_cursor_color: [f32; 4],
150 current_cursor_opacity: f32,
152 previous_cursor_opacity: f32,
154 cursor_change_time: f32,
156 current_cursor_style: CursorStyle,
158 previous_cursor_style: CursorStyle,
160 cursor_cell_width: f32,
162 cursor_cell_height: f32,
164 cursor_window_padding: f32,
166
167 cursor_shader_color: [f32; 4],
170 cursor_trail_duration: f32,
172 cursor_glow_radius: f32,
174 cursor_glow_intensity: f32,
176}
177
178impl CustomShaderRenderer {
179 #[allow(clippy::too_many_arguments)]
191 pub fn new(
192 device: &Device,
193 _queue: &Queue,
194 surface_format: TextureFormat,
195 shader_path: &Path,
196 width: u32,
197 height: u32,
198 animation_enabled: bool,
199 animation_speed: f32,
200 window_opacity: f32,
201 text_opacity: f32,
202 full_content_mode: bool,
203 ) -> Result<Self> {
204 let glsl_source = std::fs::read_to_string(shader_path)
206 .with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
207
208 let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
210
211 log::info!(
212 "Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
213 shader_path.display(),
214 glsl_source.len(),
215 wgsl_source.len()
216 );
217 log::debug!("Generated WGSL:\n{}", wgsl_source);
218
219 let module = naga::front::wgsl::parse_str(&wgsl_source)
222 .context("Custom shader WGSL parse failed")?;
223 let _info = naga::valid::Validator::new(
224 naga::valid::ValidationFlags::all(),
225 naga::valid::Capabilities::empty(),
226 )
227 .validate(&module)
228 .context("Custom shader WGSL validation failed")?;
229
230 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
231 label: Some("Custom Shader Module"),
232 source: ShaderSource::Wgsl(wgsl_source.clone().into()),
233 });
234
235 let (intermediate_texture, intermediate_texture_view) =
237 Self::create_intermediate_texture(device, surface_format, width, height);
238
239 let sampler = device.create_sampler(&SamplerDescriptor {
241 label: Some("Custom Shader Sampler"),
242 address_mode_u: AddressMode::ClampToEdge,
243 address_mode_v: AddressMode::ClampToEdge,
244 address_mode_w: AddressMode::ClampToEdge,
245 mag_filter: FilterMode::Linear,
246 min_filter: FilterMode::Linear,
247 mipmap_filter: FilterMode::Linear,
248 ..Default::default()
249 });
250
251 let uniform_buffer = device.create_buffer(&BufferDescriptor {
253 label: Some("Custom Shader Uniforms"),
254 size: std::mem::size_of::<CustomShaderUniforms>() as u64,
255 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
256 mapped_at_creation: false,
257 });
258
259 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
261 label: Some("Custom Shader Bind Group Layout"),
262 entries: &[
263 BindGroupLayoutEntry {
265 binding: 0,
266 visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
267 ty: BindingType::Buffer {
268 ty: BufferBindingType::Uniform,
269 has_dynamic_offset: false,
270 min_binding_size: None,
271 },
272 count: None,
273 },
274 BindGroupLayoutEntry {
276 binding: 1,
277 visibility: ShaderStages::FRAGMENT,
278 ty: BindingType::Texture {
279 sample_type: TextureSampleType::Float { filterable: true },
280 view_dimension: TextureViewDimension::D2,
281 multisampled: false,
282 },
283 count: None,
284 },
285 BindGroupLayoutEntry {
287 binding: 2,
288 visibility: ShaderStages::FRAGMENT,
289 ty: BindingType::Sampler(SamplerBindingType::Filtering),
290 count: None,
291 },
292 ],
293 });
294
295 let bind_group = device.create_bind_group(&BindGroupDescriptor {
297 label: Some("Custom Shader Bind Group"),
298 layout: &bind_group_layout,
299 entries: &[
300 BindGroupEntry {
301 binding: 0,
302 resource: uniform_buffer.as_entire_binding(),
303 },
304 BindGroupEntry {
305 binding: 1,
306 resource: BindingResource::TextureView(&intermediate_texture_view),
307 },
308 BindGroupEntry {
309 binding: 2,
310 resource: BindingResource::Sampler(&sampler),
311 },
312 ],
313 });
314
315 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
317 label: Some("Custom Shader Pipeline Layout"),
318 bind_group_layouts: &[&bind_group_layout],
319 push_constant_ranges: &[],
320 });
321
322 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
324 label: Some("Custom Shader Pipeline"),
325 layout: Some(&pipeline_layout),
326 vertex: VertexState {
327 module: &shader_module,
328 entry_point: Some("vs_main"),
329 buffers: &[],
330 compilation_options: Default::default(),
331 },
332 fragment: Some(FragmentState {
333 module: &shader_module,
334 entry_point: Some("fs_main"),
335 targets: &[Some(ColorTargetState {
336 format: surface_format,
337 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
338 write_mask: ColorWrites::ALL,
339 })],
340 compilation_options: Default::default(),
341 }),
342 primitive: PrimitiveState {
343 topology: PrimitiveTopology::TriangleStrip,
344 ..Default::default()
345 },
346 depth_stencil: None,
347 multisample: MultisampleState::default(),
348 multiview: None,
349 cache: None,
350 });
351
352 let now = Instant::now();
353 Ok(Self {
354 pipeline,
355 bind_group,
356 uniform_buffer,
357 intermediate_texture,
358 intermediate_texture_view,
359 start_time: now,
360 animation_enabled,
361 animation_speed,
362 texture_width: width,
363 texture_height: height,
364 surface_format,
365 bind_group_layout,
366 sampler,
367 window_opacity,
368 text_opacity,
369 full_content_mode,
370 frame_count: 0,
371 last_frame_time: now,
372 mouse_position: [0.0, 0.0],
373 mouse_click_position: [0.0, 0.0],
374 mouse_button_down: false,
375 frame_time_accumulator: 0.0,
376 frames_in_second: 0,
377 current_frame_rate: 60.0, current_cursor_pos: (0, 0),
380 previous_cursor_pos: (0, 0),
381 current_cursor_color: [1.0, 1.0, 1.0, 1.0], previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
383 current_cursor_opacity: 1.0,
384 previous_cursor_opacity: 1.0,
385 cursor_change_time: 0.0,
386 current_cursor_style: CursorStyle::SteadyBlock,
387 previous_cursor_style: CursorStyle::SteadyBlock,
388 cursor_cell_width: 10.0, cursor_cell_height: 20.0,
390 cursor_window_padding: 0.0,
391 cursor_shader_color: [1.0, 1.0, 1.0, 1.0], cursor_trail_duration: 0.5,
394 cursor_glow_radius: 80.0,
395 cursor_glow_intensity: 0.3,
396 })
397 }
398
399 fn create_intermediate_texture(
401 device: &Device,
402 format: TextureFormat,
403 width: u32,
404 height: u32,
405 ) -> (Texture, TextureView) {
406 let texture = device.create_texture(&TextureDescriptor {
407 label: Some("Custom Shader Intermediate Texture"),
408 size: Extent3d {
409 width: width.max(1),
410 height: height.max(1),
411 depth_or_array_layers: 1,
412 },
413 mip_level_count: 1,
414 sample_count: 1,
415 dimension: TextureDimension::D2,
416 format,
417 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
418 view_formats: &[],
419 });
420
421 let view = texture.create_view(&TextureViewDescriptor::default());
422 (texture, view)
423 }
424
425 pub fn intermediate_texture_view(&self) -> &TextureView {
427 &self.intermediate_texture_view
428 }
429
430 pub fn resize(&mut self, device: &Device, width: u32, height: u32) {
432 if width == self.texture_width && height == self.texture_height {
433 return;
434 }
435
436 self.texture_width = width;
437 self.texture_height = height;
438
439 let (texture, view) =
441 Self::create_intermediate_texture(device, self.surface_format, width, height);
442 self.intermediate_texture = texture;
443 self.intermediate_texture_view = view;
444
445 self.bind_group = device.create_bind_group(&BindGroupDescriptor {
447 label: Some("Custom Shader Bind Group"),
448 layout: &self.bind_group_layout,
449 entries: &[
450 BindGroupEntry {
451 binding: 0,
452 resource: self.uniform_buffer.as_entire_binding(),
453 },
454 BindGroupEntry {
455 binding: 1,
456 resource: BindingResource::TextureView(&self.intermediate_texture_view),
457 },
458 BindGroupEntry {
459 binding: 2,
460 resource: BindingResource::Sampler(&self.sampler),
461 },
462 ],
463 });
464 }
465
466 pub fn render(
471 &mut self,
472 device: &Device,
473 queue: &Queue,
474 output_view: &TextureView,
475 ) -> Result<()> {
476 let now = Instant::now();
477
478 let time = if self.animation_enabled {
480 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
481 } else {
482 0.0
483 };
484
485 let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
487 self.last_frame_time = now;
488
489 self.frame_time_accumulator += time_delta;
491 self.frames_in_second += 1;
492 if self.frame_time_accumulator >= 1.0 {
493 self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
494 self.frame_time_accumulator = 0.0;
495 self.frames_in_second = 0;
496 }
497
498 self.frame_count = self.frame_count.wrapping_add(1);
500
501 let height = self.texture_height as f32;
505 let mouse_y_flipped = height - self.mouse_position[1];
506 let click_y_flipped = height - self.mouse_click_position[1];
507
508 let mouse = if self.mouse_button_down {
509 [
511 self.mouse_position[0],
512 mouse_y_flipped,
513 self.mouse_click_position[0],
514 click_y_flipped,
515 ]
516 } else {
517 [
519 self.mouse_position[0],
520 mouse_y_flipped,
521 -self.mouse_click_position[0].abs(),
522 -click_y_flipped.abs(),
523 ]
524 };
525
526 let date = {
529 use std::time::{SystemTime, UNIX_EPOCH};
530 let now_sys = SystemTime::now();
531 let since_epoch = now_sys.duration_since(UNIX_EPOCH).unwrap_or_default();
532 let secs = since_epoch.as_secs();
533
534 let days_since_epoch = secs / 86400;
537 let secs_today = (secs % 86400) as f32;
538
539 let mut year = 1970i32;
542 let mut remaining_days = days_since_epoch as i32;
543
544 loop {
545 let days_in_year = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
546 366
547 } else {
548 365
549 };
550 if remaining_days < days_in_year {
551 break;
552 }
553 remaining_days -= days_in_year;
554 year += 1;
555 }
556
557 let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
558 let days_in_months: [i32; 12] = if is_leap {
559 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
560 } else {
561 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
562 };
563
564 let mut month = 0i32;
565 for (i, &days) in days_in_months.iter().enumerate() {
566 if remaining_days < days {
567 month = i as i32;
568 break;
569 }
570 remaining_days -= days;
571 }
572
573 let day = remaining_days + 1; [year as f32, month as f32, day as f32, secs_today]
576 };
577
578 let (curr_x, curr_y) =
580 self.cursor_to_pixels(self.current_cursor_pos.0, self.current_cursor_pos.1);
581 let (prev_x, prev_y) =
582 self.cursor_to_pixels(self.previous_cursor_pos.0, self.previous_cursor_pos.1);
583
584 if self.frame_count.is_multiple_of(60) {
586 log::debug!(
587 "CURSOR_SHADER: pos=({},{}) -> pixels=({:.1},{:.1}), cell=({:.1}x{:.1}), padding={:.1}, resolution={}x{}",
588 self.current_cursor_pos.0, self.current_cursor_pos.1,
589 curr_x, curr_y,
590 self.cursor_cell_width, self.cursor_cell_height,
591 self.cursor_window_padding,
592 self.texture_width, self.texture_height
593 );
594 }
595
596 let uniforms = CustomShaderUniforms {
598 resolution: [self.texture_width as f32, self.texture_height as f32],
599 time,
600 time_delta,
601 mouse,
602 date,
603 opacity: self.window_opacity,
604 text_opacity: self.text_opacity,
605 full_content_mode: if self.full_content_mode { 1.0 } else { 0.0 },
606 frame: self.frame_count as f32,
607 frame_rate: self.current_frame_rate,
608 resolution_z: 1.0, _pad1: [0.0, 0.0],
610 current_cursor: [
616 curr_x,
617 curr_y,
618 self.cursor_width_for_style(self.current_cursor_style),
619 self.cursor_height_for_style(self.current_cursor_style),
620 ],
621 previous_cursor: [
622 prev_x,
623 prev_y,
624 self.cursor_width_for_style(self.previous_cursor_style),
625 self.cursor_height_for_style(self.previous_cursor_style),
626 ],
627 current_cursor_color: [
628 self.current_cursor_color[0],
629 self.current_cursor_color[1],
630 self.current_cursor_color[2],
631 self.current_cursor_color[3] * self.current_cursor_opacity,
632 ],
633 previous_cursor_color: [
634 self.previous_cursor_color[0],
635 self.previous_cursor_color[1],
636 self.previous_cursor_color[2],
637 self.previous_cursor_color[3] * self.previous_cursor_opacity,
638 ],
639 cursor_change_time: self.cursor_change_time,
640 cursor_trail_duration: self.cursor_trail_duration,
642 cursor_glow_radius: self.cursor_glow_radius,
643 cursor_glow_intensity: self.cursor_glow_intensity,
644 cursor_shader_color: self.cursor_shader_color,
645 };
646
647 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
648
649 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
651 label: Some("Custom Shader Encoder"),
652 });
653
654 {
656 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
657 label: Some("Custom Shader Render Pass"),
658 color_attachments: &[Some(RenderPassColorAttachment {
659 view: output_view,
660 resolve_target: None,
661 ops: Operations {
662 load: LoadOp::Clear(Color::TRANSPARENT),
664 store: StoreOp::Store,
665 },
666 depth_slice: None,
667 })],
668 depth_stencil_attachment: None,
669 timestamp_writes: None,
670 occlusion_query_set: None,
671 });
672
673 render_pass.set_pipeline(&self.pipeline);
674 render_pass.set_bind_group(0, &self.bind_group, &[]);
675 render_pass.draw(0..4, 0..1);
676 }
677
678 queue.submit(std::iter::once(encoder.finish()));
679
680 Ok(())
681 }
682
683 #[allow(dead_code)]
685 pub fn animation_enabled(&self) -> bool {
686 self.animation_enabled
687 }
688
689 #[allow(dead_code)]
691 pub fn set_animation_enabled(&mut self, enabled: bool) {
692 self.animation_enabled = enabled;
693 if enabled {
694 self.start_time = Instant::now();
696 }
697 }
698
699 pub fn set_animation_speed(&mut self, speed: f32) {
701 self.animation_speed = speed.max(0.0);
702 }
703
704 pub fn set_opacity(&mut self, opacity: f32) {
706 self.window_opacity = opacity.clamp(0.0, 1.0);
707 }
708
709 pub fn set_full_content_mode(&mut self, enabled: bool) {
711 self.full_content_mode = enabled;
712 }
713
714 #[allow(dead_code)]
716 pub fn full_content_mode(&self) -> bool {
717 self.full_content_mode
718 }
719
720 pub fn set_mouse_position(&mut self, x: f32, y: f32) {
726 self.mouse_position = [x, y];
727 }
728
729 pub fn set_mouse_button(&mut self, pressed: bool, x: f32, y: f32) {
738 self.mouse_button_down = pressed;
739 if pressed {
740 self.mouse_click_position = [x, y];
742 }
743 }
744
745 pub fn update_cursor(
759 &mut self,
760 col: usize,
761 row: usize,
762 opacity: f32,
763 cursor_color: [f32; 4],
764 style: CursorStyle,
765 ) {
766 let new_pos = (col, row);
767 let style_changed = style != self.current_cursor_style;
768 let pos_changed = new_pos != self.current_cursor_pos;
769
770 if pos_changed || style_changed {
771 self.previous_cursor_pos = self.current_cursor_pos;
773 self.previous_cursor_opacity = self.current_cursor_opacity;
774 self.previous_cursor_color = self.current_cursor_color;
775 self.previous_cursor_style = self.current_cursor_style;
776 self.current_cursor_pos = new_pos;
777 self.current_cursor_style = style;
778
779 self.cursor_change_time = if self.animation_enabled {
781 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
782 } else {
783 0.0
784 };
785
786 if pos_changed {
787 log::trace!(
788 "Cursor moved: ({}, {}) -> ({}, {}), change_time={:.3}",
789 self.previous_cursor_pos.0,
790 self.previous_cursor_pos.1,
791 col,
792 row,
793 self.cursor_change_time
794 );
795 }
796 }
797 self.current_cursor_opacity = opacity;
798 self.current_cursor_color = cursor_color;
799 }
800
801 pub fn update_cell_dimensions(&mut self, cell_width: f32, cell_height: f32, padding: f32) {
808 self.cursor_cell_width = cell_width;
809 self.cursor_cell_height = cell_height;
810 self.cursor_window_padding = padding;
811 }
812
813 fn cursor_to_pixels(&self, col: usize, row: usize) -> (f32, f32) {
817 let x = self.cursor_window_padding + (col as f32 * self.cursor_cell_width);
818 let y = self.cursor_window_padding + (row as f32 * self.cursor_cell_height);
819 (x, y)
820 }
821
822 fn cursor_width_for_style(&self, style: CursorStyle) -> f32 {
824 match style {
825 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => self.cursor_cell_width,
827 CursorStyle::SteadyBar | CursorStyle::BlinkingBar => 2.0,
829 CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => self.cursor_cell_width,
831 }
832 }
833
834 fn cursor_height_for_style(&self, style: CursorStyle) -> f32 {
836 match style {
837 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => self.cursor_cell_height,
839 CursorStyle::SteadyBar | CursorStyle::BlinkingBar => self.cursor_cell_height,
841 CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => 2.0,
843 }
844 }
845
846 pub fn cursor_needs_animation(&self) -> bool {
851 if self.animation_enabled {
852 let current_time =
853 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0);
854 (current_time - self.cursor_change_time) < 1.0
856 } else {
857 false
858 }
859 }
860
861 pub fn update_cursor_shader_config(
869 &mut self,
870 color: [u8; 3],
871 trail_duration: f32,
872 glow_radius: f32,
873 glow_intensity: f32,
874 ) {
875 self.cursor_shader_color = [
876 color[0] as f32 / 255.0,
877 color[1] as f32 / 255.0,
878 color[2] as f32 / 255.0,
879 1.0,
880 ];
881 self.cursor_trail_duration = trail_duration.max(0.0);
882 self.cursor_glow_radius = glow_radius.max(0.0);
883 self.cursor_glow_intensity = glow_intensity.clamp(0.0, 1.0);
884 }
885
886 pub fn reload_from_source(&mut self, device: &Device, source: &str, name: &str) -> Result<()> {
899 let wgsl_source = transpile_glsl_to_wgsl_source(source, name)?;
901
902 log::info!(
903 "Reloading custom shader from source ({} bytes GLSL -> {} bytes WGSL)",
904 source.len(),
905 wgsl_source.len()
906 );
907 log::debug!("Generated WGSL:\n{}", wgsl_source);
908
909 let module = naga::front::wgsl::parse_str(&wgsl_source)
911 .context("Custom shader WGSL parse failed")?;
912 let _info = naga::valid::Validator::new(
913 naga::valid::ValidationFlags::all(),
914 naga::valid::Capabilities::empty(),
915 )
916 .validate(&module)
917 .context("Custom shader WGSL validation failed")?;
918
919 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
921 label: Some("Custom Shader Module (reloaded)"),
922 source: ShaderSource::Wgsl(wgsl_source.into()),
923 });
924
925 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
927 label: Some("Custom Shader Pipeline Layout (reloaded)"),
928 bind_group_layouts: &[&self.bind_group_layout],
929 push_constant_ranges: &[],
930 });
931
932 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
934 label: Some("Custom Shader Pipeline (reloaded)"),
935 layout: Some(&pipeline_layout),
936 vertex: VertexState {
937 module: &shader_module,
938 entry_point: Some("vs_main"),
939 buffers: &[],
940 compilation_options: Default::default(),
941 },
942 fragment: Some(FragmentState {
943 module: &shader_module,
944 entry_point: Some("fs_main"),
945 targets: &[Some(ColorTargetState {
946 format: self.surface_format,
947 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
948 write_mask: ColorWrites::ALL,
949 })],
950 compilation_options: Default::default(),
951 }),
952 primitive: PrimitiveState {
953 topology: PrimitiveTopology::TriangleStrip,
954 ..Default::default()
955 },
956 depth_stencil: None,
957 multisample: MultisampleState::default(),
958 multiview: None,
959 cache: None,
960 });
961
962 self.pipeline = pipeline;
964
965 self.start_time = Instant::now();
967
968 log::info!("Custom shader reloaded successfully from source");
969 Ok(())
970 }
971}
972
973fn transpile_glsl_to_wgsl(glsl_source: &str, shader_path: &Path) -> Result<String> {
1002 let wrapped_glsl = format!(
1010 r#"#version 450
1011
1012// Uniforms - must match Rust struct layout (std140)
1013// Total size: 192 bytes
1014layout(set = 0, binding = 0) uniform Uniforms {{
1015 vec2 iResolution; // offset 0, size 8 - Viewport resolution
1016 float iTime; // offset 8, size 4 - Time in seconds
1017 float iTimeDelta; // offset 12, size 4 - Time since last frame
1018 vec4 iMouse; // offset 16, size 16 - Mouse state (xy=current, zw=click)
1019 vec4 iDate; // offset 32, size 16 - Date (year, month, day, seconds)
1020 float iOpacity; // offset 48, size 4 - Window opacity
1021 float iTextOpacity; // offset 52, size 4 - Text opacity
1022 float iFullContent; // offset 56, size 4 - Full content mode (1.0 = enabled)
1023 float iFrame; // offset 60, size 4 - Frame counter
1024 float iFrameRate; // offset 64, size 4 - Current FPS
1025 float iResolutionZ; // offset 68, size 4 - Pixel aspect ratio (usually 1.0)
1026 vec2 _pad1; // offset 72, size 8 - Padding
1027
1028 // Cursor uniforms (Ghostty-compatible, v1.2.0+)
1029 vec4 iCurrentCursor; // offset 80, size 16 - xy=position, zw=size (pixels)
1030 vec4 iPreviousCursor; // offset 96, size 16 - xy=previous position, zw=size
1031 vec4 iCurrentCursorColor; // offset 112, size 16 - RGBA (opacity baked into alpha)
1032 vec4 iPreviousCursorColor; // offset 128, size 16 - RGBA previous color
1033 float iTimeCursorChange; // offset 144, size 4 - Time when cursor last moved
1034
1035 // Cursor shader configuration uniforms
1036 float iCursorTrailDuration;// offset 148, size 4 - Trail effect duration (seconds)
1037 float iCursorGlowRadius; // offset 152, size 4 - Glow effect radius (pixels)
1038 float iCursorGlowIntensity;// offset 156, size 4 - Glow effect intensity (0-1)
1039 vec4 iCursorShaderColor; // offset 160, size 16 - User-configured cursor color (aligned to 16)
1040}}; // total: 176 bytes
1041
1042// Terminal content texture (iChannel0)
1043layout(set = 0, binding = 1) uniform texture2D _iChannel0Tex;
1044layout(set = 0, binding = 2) uniform sampler _iChannel0Sampler;
1045
1046// Combined sampler for texture() calls
1047#define iChannel0 sampler2D(_iChannel0Tex, _iChannel0Sampler)
1048
1049// Input from vertex shader
1050layout(location = 0) in vec2 v_uv;
1051
1052// Output color
1053layout(location = 0) out vec4 outColor;
1054
1055// ============ User shader code begins ============
1056
1057{glsl_source}
1058
1059// ============ User shader code ends ============
1060
1061void main() {{
1062 vec2 fragCoord = v_uv * iResolution;
1063 vec4 shaderColor;
1064 mainImage(shaderColor, fragCoord);
1065
1066 if (iFullContent > 0.5) {{
1067 // Full content mode: shader output is used directly
1068 // The shader has full control over the terminal content via iChannel0
1069 // Apply window opacity to the shader's alpha output
1070 outColor = vec4(shaderColor.rgb * iOpacity, shaderColor.a * iOpacity);
1071 }} else {{
1072 // Background-only mode: text is composited cleanly on top
1073 // Sample terminal to detect text pixels
1074 vec4 terminalColor = texture(iChannel0, v_uv);
1075 float hasText = step(0.01, terminalColor.a);
1076
1077 // Text pixels: use terminal color with text opacity
1078 // Background pixels: use shader output with window opacity
1079 vec3 textCol = terminalColor.rgb;
1080 vec3 bgCol = shaderColor.rgb;
1081
1082 // Composite: text over shader background
1083 float textA = hasText * iTextOpacity;
1084 float bgA = (1.0 - hasText) * iOpacity;
1085
1086 vec3 finalRgb = textCol * textA + bgCol * bgA;
1087 float finalA = textA + bgA;
1088
1089 outColor = vec4(finalRgb, finalA);
1090 }}
1091}}
1092"#
1093 );
1094
1095 let mut parser = naga::front::glsl::Frontend::default();
1097 let options = naga::front::glsl::Options::from(naga::ShaderStage::Fragment);
1098
1099 let module = parser.parse(&options, &wrapped_glsl).map_err(|errors| {
1100 let error_messages: Vec<String> = errors
1101 .errors
1102 .iter()
1103 .map(|e| format!(" {:?}", e.kind))
1104 .collect();
1105 anyhow::anyhow!(
1106 "GLSL parse error in '{}'. Errors:\n{}",
1107 shader_path.display(),
1108 error_messages.join("\n")
1109 )
1110 })?;
1111
1112 let info = naga::valid::Validator::new(
1114 naga::valid::ValidationFlags::all(),
1115 naga::valid::Capabilities::all(),
1116 )
1117 .validate(&module)
1118 .map_err(|e| {
1119 anyhow::anyhow!(
1120 "Shader validation failed for '{}': {:?}",
1121 shader_path.display(),
1122 e
1123 )
1124 })?;
1125
1126 let mut fragment_wgsl = String::new();
1128 let mut writer =
1129 naga::back::wgsl::Writer::new(&mut fragment_wgsl, naga::back::wgsl::WriterFlags::empty());
1130
1131 writer.write(&module, &info).map_err(|e| {
1132 anyhow::anyhow!(
1133 "WGSL generation failed for '{}': {:?}",
1134 shader_path.display(),
1135 e
1136 )
1137 })?;
1138
1139 let fragment_wgsl = fragment_wgsl.replace("fn main(", "fn fs_main(");
1142
1143 let full_wgsl = format!(
1145 r#"// Auto-generated WGSL from GLSL shader: {}
1146
1147struct VertexOutput {{
1148 @builtin(position) position: vec4<f32>,
1149 @location(0) uv: vec2<f32>,
1150}}
1151
1152@vertex
1153fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {{
1154 var out: VertexOutput;
1155
1156 // Generate full-screen quad vertices (triangle strip)
1157 let x = f32(vertex_index & 1u);
1158 let y = f32((vertex_index >> 1u) & 1u);
1159
1160 // Full screen in NDC
1161 out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
1162 out.uv = vec2<f32>(x, y);
1163
1164 return out;
1165}}
1166
1167// ============ Fragment shader (transpiled from GLSL) ============
1168
1169{fragment_wgsl}
1170"#,
1171 shader_path.display()
1172 );
1173
1174 Ok(full_wgsl)
1175}
1176
1177fn transpile_glsl_to_wgsl_source(glsl_source: &str, name: &str) -> Result<String> {
1181 let wrapped_glsl = format!(
1183 r#"#version 450
1184
1185// Uniforms - must match Rust struct layout (std140)
1186// Total size: 192 bytes
1187layout(set = 0, binding = 0) uniform Uniforms {{
1188 vec2 iResolution; // offset 0, size 8 - Viewport resolution
1189 float iTime; // offset 8, size 4 - Time in seconds
1190 float iTimeDelta; // offset 12, size 4 - Time since last frame
1191 vec4 iMouse; // offset 16, size 16 - Mouse state (xy=current, zw=click)
1192 vec4 iDate; // offset 32, size 16 - Date (year, month, day, seconds)
1193 float iOpacity; // offset 48, size 4 - Window opacity
1194 float iTextOpacity; // offset 52, size 4 - Text opacity
1195 float iFullContent; // offset 56, size 4 - Full content mode (1.0 = enabled)
1196 float iFrame; // offset 60, size 4 - Frame counter
1197 float iFrameRate; // offset 64, size 4 - Current FPS
1198 float iResolutionZ; // offset 68, size 4 - Pixel aspect ratio (usually 1.0)
1199 vec2 _pad1; // offset 72, size 8 - Padding
1200
1201 // Cursor uniforms (Ghostty-compatible, v1.2.0+)
1202 vec4 iCurrentCursor; // offset 80, size 16 - xy=position, zw=size (pixels)
1203 vec4 iPreviousCursor; // offset 96, size 16 - xy=previous position, zw=size
1204 vec4 iCurrentCursorColor; // offset 112, size 16 - RGBA (opacity baked into alpha)
1205 vec4 iPreviousCursorColor; // offset 128, size 16 - RGBA previous color
1206 float iTimeCursorChange; // offset 144, size 4 - Time when cursor last moved
1207
1208 // Cursor shader configuration uniforms
1209 float iCursorTrailDuration;// offset 148, size 4 - Trail effect duration (seconds)
1210 float iCursorGlowRadius; // offset 152, size 4 - Glow effect radius (pixels)
1211 float iCursorGlowIntensity;// offset 156, size 4 - Glow effect intensity (0-1)
1212 vec4 iCursorShaderColor; // offset 160, size 16 - User-configured cursor color (aligned to 16)
1213}}; // total: 176 bytes
1214
1215// Terminal content texture (iChannel0)
1216layout(set = 0, binding = 1) uniform texture2D _iChannel0Tex;
1217layout(set = 0, binding = 2) uniform sampler _iChannel0Sampler;
1218
1219// Combined sampler for texture() calls
1220#define iChannel0 sampler2D(_iChannel0Tex, _iChannel0Sampler)
1221
1222// Input from vertex shader
1223layout(location = 0) in vec2 v_uv;
1224
1225// Output color
1226layout(location = 0) out vec4 outColor;
1227
1228// ============ User shader code begins ============
1229
1230{glsl_source}
1231
1232// ============ User shader code ends ============
1233
1234void main() {{
1235 vec2 fragCoord = v_uv * iResolution;
1236 vec4 shaderColor;
1237 mainImage(shaderColor, fragCoord);
1238
1239 if (iFullContent > 0.5) {{
1240 // Full content mode: shader output is used directly
1241 // The shader has full control over the terminal content via iChannel0
1242 // Apply window opacity to the shader's alpha output
1243 outColor = vec4(shaderColor.rgb * iOpacity, shaderColor.a * iOpacity);
1244 }} else {{
1245 // Background-only mode: text is composited cleanly on top
1246 // Sample terminal to detect text pixels
1247 vec4 terminalColor = texture(iChannel0, v_uv);
1248 float hasText = step(0.01, terminalColor.a);
1249
1250 // Text pixels: use terminal color with text opacity
1251 // Background pixels: use shader output with window opacity
1252 vec3 textCol = terminalColor.rgb;
1253 vec3 bgCol = shaderColor.rgb;
1254
1255 // Composite: text over shader background
1256 float textA = hasText * iTextOpacity;
1257 float bgA = (1.0 - hasText) * iOpacity;
1258
1259 vec3 finalRgb = textCol * textA + bgCol * bgA;
1260 float finalA = textA + bgA;
1261
1262 outColor = vec4(finalRgb, finalA);
1263 }}
1264}}
1265"#
1266 );
1267
1268 let mut parser = naga::front::glsl::Frontend::default();
1270 let options = naga::front::glsl::Options::from(naga::ShaderStage::Fragment);
1271
1272 let module = parser.parse(&options, &wrapped_glsl).map_err(|errors| {
1273 let error_messages: Vec<String> = errors
1274 .errors
1275 .iter()
1276 .map(|e| format!(" {:?}", e.kind))
1277 .collect();
1278 anyhow::anyhow!(
1279 "GLSL parse error in '{}'. Errors:\n{}",
1280 name,
1281 error_messages.join("\n")
1282 )
1283 })?;
1284
1285 let info = naga::valid::Validator::new(
1287 naga::valid::ValidationFlags::all(),
1288 naga::valid::Capabilities::all(),
1289 )
1290 .validate(&module)
1291 .map_err(|e| anyhow::anyhow!("Shader validation failed for '{}': {:?}", name, e))?;
1292
1293 let mut fragment_wgsl = String::new();
1295 let mut writer =
1296 naga::back::wgsl::Writer::new(&mut fragment_wgsl, naga::back::wgsl::WriterFlags::empty());
1297
1298 writer
1299 .write(&module, &info)
1300 .map_err(|e| anyhow::anyhow!("WGSL generation failed for '{}': {:?}", name, e))?;
1301
1302 let fragment_wgsl = fragment_wgsl.replace("fn main(", "fn fs_main(");
1305
1306 let full_wgsl = format!(
1308 r#"// Auto-generated WGSL from GLSL shader: {}
1309
1310struct VertexOutput {{
1311 @builtin(position) position: vec4<f32>,
1312 @location(0) uv: vec2<f32>,
1313}}
1314
1315@vertex
1316fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {{
1317 var out: VertexOutput;
1318
1319 // Generate full-screen quad vertices (triangle strip)
1320 let x = f32(vertex_index & 1u);
1321 let y = f32((vertex_index >> 1u) & 1u);
1322
1323 // Full screen in NDC
1324 out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
1325 out.uv = vec2<f32>(x, y);
1326
1327 return out;
1328}}
1329
1330// ============ Fragment shader (transpiled from GLSL) ============
1331
1332{fragment_wgsl}
1333"#,
1334 name
1335 );
1336
1337 Ok(full_wgsl)
1338}