1use anyhow::{Context, Result};
18use par_term_emu_core_rust::cursor::CursorStyle;
19use std::path::Path;
20use std::time::Instant;
21use wgpu::*;
22
23mod cubemap;
24mod cursor;
25pub mod pipeline;
26pub mod textures;
27pub mod transpiler;
28pub mod types;
29
30use cubemap::CubemapTexture;
31use pipeline::{create_bind_group, create_bind_group_layout, create_render_pipeline};
32use textures::{ChannelTexture, load_channel_textures};
33use transpiler::{transpile_glsl_to_wgsl, transpile_glsl_to_wgsl_source};
34use types::CustomShaderUniforms;
35
36pub struct CustomShaderRenderer {
38 pub(crate) pipeline: RenderPipeline,
40 pub(crate) bind_group: BindGroup,
42 pub(crate) uniform_buffer: Buffer,
44 pub(crate) intermediate_texture: Texture,
46 pub(crate) intermediate_texture_view: TextureView,
48 pub(crate) start_time: Instant,
50 pub(crate) animation_enabled: bool,
52 pub(crate) animation_speed: f32,
54 pub(crate) texture_width: u32,
56 pub(crate) texture_height: u32,
57 pub(crate) surface_format: TextureFormat,
59 pub(crate) bind_group_layout: BindGroupLayout,
61 pub(crate) sampler: Sampler,
63 pub(crate) scale_factor: f32,
65 pub(crate) window_opacity: f32,
67 pub(crate) keep_text_opaque: bool,
69 pub(crate) full_content_mode: bool,
71 pub(crate) brightness: f32,
73 pub(crate) frame_count: u32,
75 pub(crate) last_frame_time: Instant,
77 pub(crate) mouse_position: [f32; 2],
79 pub(crate) mouse_click_position: [f32; 2],
81 pub(crate) mouse_button_down: bool,
83 pub(crate) frame_time_accumulator: f32,
85 pub(crate) frames_in_second: u32,
87 pub(crate) current_frame_rate: f32,
89
90 pub(crate) current_cursor_pos: (usize, usize),
93 pub(crate) previous_cursor_pos: (usize, usize),
95 pub(crate) current_cursor_color: [f32; 4],
97 pub(crate) previous_cursor_color: [f32; 4],
99 pub(crate) current_cursor_opacity: f32,
101 pub(crate) previous_cursor_opacity: f32,
103 pub(crate) cursor_change_time: f32,
105 pub(crate) current_cursor_style: CursorStyle,
107 pub(crate) previous_cursor_style: CursorStyle,
109 pub(crate) cursor_cell_width: f32,
111 pub(crate) cursor_cell_height: f32,
113 pub(crate) cursor_window_padding: f32,
115 pub(crate) cursor_content_offset_y: f32,
117 pub(crate) cursor_content_offset_x: f32,
119
120 pub(crate) cursor_shader_color: [f32; 4],
123 pub(crate) cursor_trail_duration: f32,
125 pub(crate) cursor_glow_radius: f32,
127 pub(crate) cursor_glow_intensity: f32,
129
130 pub(crate) key_press_time: f32,
133
134 pub(crate) channel_textures: [ChannelTexture; 4],
137
138 pub(crate) cubemap: CubemapTexture,
141
142 pub(crate) use_background_as_channel0: bool,
145 pub(crate) background_channel_texture: Option<ChannelTexture>,
148
149 pub(crate) background_color: [f32; 4],
154
155 pub(crate) progress_data: [f32; 4],
158
159 pub(crate) content_inset_right: f32,
163}
164
165impl CustomShaderRenderer {
166 #[allow(clippy::too_many_arguments)]
168 pub fn new(
169 device: &Device,
170 queue: &Queue,
171 surface_format: TextureFormat,
172 shader_path: &Path,
173 width: u32,
174 height: u32,
175 animation_enabled: bool,
176 animation_speed: f32,
177 window_opacity: f32,
178 full_content_mode: bool,
179 channel_paths: &[Option<std::path::PathBuf>; 4],
180 cubemap_path: Option<&Path>,
181 ) -> Result<Self> {
182 let glsl_source = std::fs::read_to_string(shader_path)
184 .with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
185
186 let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
188
189 log::info!(
190 "Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
191 shader_path.display(),
192 glsl_source.len(),
193 wgsl_source.len()
194 );
195 log::debug!("Generated WGSL:\n{}", wgsl_source);
196
197 let shader_name = shader_path
199 .file_stem()
200 .and_then(|s| s.to_str())
201 .unwrap_or("unknown");
202 let debug_filename = format!("/tmp/par_term_{}_shader.wgsl", shader_name);
203 if let Err(e) = std::fs::write(&debug_filename, &wgsl_source) {
204 log::warn!("Failed to write debug shader: {}", e);
205 } else {
206 log::info!("Wrote debug shader to {}", debug_filename);
207 }
208
209 let module = naga::front::wgsl::parse_str(&wgsl_source)
211 .context("Custom shader WGSL parse failed")?;
212 let _info = naga::valid::Validator::new(
213 naga::valid::ValidationFlags::all(),
214 naga::valid::Capabilities::empty(),
215 )
216 .validate(&module)
217 .context("Custom shader WGSL validation failed")?;
218
219 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
220 label: Some("Custom Shader Module"),
221 source: ShaderSource::Wgsl(wgsl_source.clone().into()),
222 });
223
224 let (intermediate_texture, intermediate_texture_view) =
226 Self::create_intermediate_texture(device, surface_format, width, height);
227
228 let sampler = device.create_sampler(&SamplerDescriptor {
231 label: Some("Custom Shader Sampler"),
232 address_mode_u: AddressMode::ClampToEdge,
233 address_mode_v: AddressMode::ClampToEdge,
234 address_mode_w: AddressMode::ClampToEdge,
235 mag_filter: FilterMode::Nearest,
236 min_filter: FilterMode::Nearest,
237 mipmap_filter: FilterMode::Nearest,
238 ..Default::default()
239 });
240
241 let channel_textures = load_channel_textures(device, queue, channel_paths);
243
244 let cubemap = match cubemap_path {
246 Some(path) => match CubemapTexture::from_prefix(device, queue, path) {
247 Ok(cm) => cm,
248 Err(e) => {
249 log::error!("Failed to load cubemap '{}': {}", path.display(), e);
250 CubemapTexture::placeholder(device, queue)
251 }
252 },
253 None => CubemapTexture::placeholder(device, queue),
254 };
255
256 let uniform_buffer = device.create_buffer(&BufferDescriptor {
258 label: Some("Custom Shader Uniforms"),
259 size: std::mem::size_of::<CustomShaderUniforms>() as u64,
260 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
261 mapped_at_creation: false,
262 });
263
264 let bind_group_layout = create_bind_group_layout(device);
266 let bind_group = create_bind_group(
267 device,
268 &bind_group_layout,
269 &uniform_buffer,
270 &intermediate_texture_view,
271 &sampler,
272 &channel_textures,
273 &cubemap,
274 );
275
276 let pipeline = create_render_pipeline(
278 device,
279 &shader_module,
280 &bind_group_layout,
281 surface_format,
282 Some("Custom Shader Pipeline"),
283 );
284
285 let now = Instant::now();
286 Ok(Self {
287 pipeline,
288 bind_group,
289 uniform_buffer,
290 intermediate_texture,
291 intermediate_texture_view,
292 start_time: now,
293 animation_enabled,
294 animation_speed,
295 texture_width: width,
296 texture_height: height,
297 surface_format,
298 bind_group_layout,
299 sampler,
300 window_opacity,
301 keep_text_opaque: false,
302 scale_factor: 1.0,
303 full_content_mode,
304 brightness: 1.0,
305 frame_count: 0,
306 last_frame_time: now,
307 mouse_position: [0.0, 0.0],
308 mouse_click_position: [0.0, 0.0],
309 mouse_button_down: false,
310 frame_time_accumulator: 0.0,
311 frames_in_second: 0,
312 current_frame_rate: 60.0,
313 current_cursor_pos: (0, 0),
314 previous_cursor_pos: (0, 0),
315 current_cursor_color: [1.0, 1.0, 1.0, 1.0],
316 previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
317 current_cursor_opacity: 1.0,
318 previous_cursor_opacity: 1.0,
319 cursor_change_time: 0.0,
320 current_cursor_style: CursorStyle::SteadyBlock,
321 previous_cursor_style: CursorStyle::SteadyBlock,
322 cursor_cell_width: 10.0,
323 cursor_cell_height: 20.0,
324 cursor_window_padding: 0.0,
325 cursor_content_offset_y: 0.0,
326 cursor_content_offset_x: 0.0,
327 cursor_shader_color: [1.0, 1.0, 1.0, 1.0],
328 cursor_trail_duration: 0.5,
329 cursor_glow_radius: 80.0,
330 cursor_glow_intensity: 0.3,
331 key_press_time: 0.0,
332 channel_textures,
333 cubemap,
334 use_background_as_channel0: false,
335 background_channel_texture: None,
336 background_color: [0.0, 0.0, 0.0, 0.0], progress_data: [0.0, 0.0, 0.0, 0.0],
338 content_inset_right: 0.0,
339 })
340 }
341
342 fn create_intermediate_texture(
344 device: &Device,
345 format: TextureFormat,
346 width: u32,
347 height: u32,
348 ) -> (Texture, TextureView) {
349 let texture = device.create_texture(&TextureDescriptor {
350 label: Some("Custom Shader Intermediate Texture"),
351 size: Extent3d {
352 width: width.max(1),
353 height: height.max(1),
354 depth_or_array_layers: 1,
355 },
356 mip_level_count: 1,
357 sample_count: 1,
358 dimension: TextureDimension::D2,
359 format,
360 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
361 view_formats: &[],
362 });
363
364 let view = texture.create_view(&TextureViewDescriptor::default());
365 (texture, view)
366 }
367
368 pub fn intermediate_texture_view(&self) -> &TextureView {
370 &self.intermediate_texture_view
371 }
372
373 pub fn clear_intermediate_texture(&self, device: &Device, queue: &Queue) {
377 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
378 label: Some("Clear Intermediate Texture Encoder"),
379 });
380
381 {
382 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
383 label: Some("Clear Intermediate Texture Pass"),
384 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
385 view: &self.intermediate_texture_view,
386 resolve_target: None,
387 ops: wgpu::Operations {
388 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
389 store: wgpu::StoreOp::Store,
390 },
391 depth_slice: None,
392 })],
393 depth_stencil_attachment: None,
394 timestamp_writes: None,
395 occlusion_query_set: None,
396 });
397 }
398
399 queue.submit(std::iter::once(encoder.finish()));
400 }
401
402 pub fn resize(&mut self, device: &Device, width: u32, height: u32) {
404 if width == self.texture_width && height == self.texture_height {
405 return;
406 }
407
408 self.texture_width = width;
409 self.texture_height = height;
410
411 let (texture, view) =
413 Self::create_intermediate_texture(device, self.surface_format, width, height);
414 self.intermediate_texture = texture;
415 self.intermediate_texture_view = view;
416
417 self.recreate_bind_group(device);
419 }
420
421 pub fn render(
431 &mut self,
432 device: &Device,
433 queue: &Queue,
434 output_view: &TextureView,
435 apply_opacity: bool,
436 ) -> Result<()> {
437 self.render_with_clear_color(
438 device,
439 queue,
440 output_view,
441 apply_opacity,
442 Color::TRANSPARENT,
443 )
444 }
445
446 pub fn render_with_clear_color(
449 &mut self,
450 device: &Device,
451 queue: &Queue,
452 output_view: &TextureView,
453 apply_opacity: bool,
454 clear_color: Color,
455 ) -> Result<()> {
456 let now = Instant::now();
457
458 let time = if self.animation_enabled {
460 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
461 } else {
462 0.0
463 };
464
465 let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
467 self.last_frame_time = now;
468
469 self.frame_time_accumulator += time_delta;
471 self.frames_in_second += 1;
472 if self.frame_time_accumulator >= 1.0 {
473 self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
474 self.frame_time_accumulator = 0.0;
475 self.frames_in_second = 0;
476 }
477
478 self.frame_count = self.frame_count.wrapping_add(1);
479
480 let uniforms = self.build_uniforms(time, time_delta, apply_opacity);
482 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
483
484 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
486 label: Some("Custom Shader Encoder"),
487 });
488
489 {
490 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
491 label: Some("Custom Shader Render Pass"),
492 color_attachments: &[Some(RenderPassColorAttachment {
493 view: output_view,
494 resolve_target: None,
495 ops: Operations {
496 load: LoadOp::Clear(clear_color),
497 store: StoreOp::Store,
498 },
499 depth_slice: None,
500 })],
501 depth_stencil_attachment: None,
502 timestamp_writes: None,
503 occlusion_query_set: None,
504 });
505
506 render_pass.set_pipeline(&self.pipeline);
512 render_pass.set_bind_group(0, &self.bind_group, &[]);
513 render_pass.draw(0..4, 0..1);
514 }
515
516 queue.submit(std::iter::once(encoder.finish()));
517 Ok(())
518 }
519
520 fn build_uniforms(
522 &self,
523 time: f32,
524 time_delta: f32,
525 apply_opacity: bool,
526 ) -> CustomShaderUniforms {
527 let height = self.texture_height as f32;
529 let mouse_y_flipped = height - self.mouse_position[1];
530 let click_y_flipped = height - self.mouse_click_position[1];
531
532 let mouse = if self.mouse_button_down {
533 [
534 self.mouse_position[0],
535 mouse_y_flipped,
536 self.mouse_click_position[0],
537 click_y_flipped,
538 ]
539 } else {
540 [
541 self.mouse_position[0],
542 mouse_y_flipped,
543 -self.mouse_click_position[0].abs(),
544 -click_y_flipped.abs(),
545 ]
546 };
547
548 let date = Self::calculate_date();
550
551 let (curr_x, curr_y) =
553 self.cursor_to_pixels(self.current_cursor_pos.0, self.current_cursor_pos.1);
554 let (prev_x, prev_y) =
555 self.cursor_to_pixels(self.previous_cursor_pos.0, self.previous_cursor_pos.1);
556
557 let effective_opacity = if apply_opacity {
563 self.window_opacity
564 } else {
565 0.0 };
567
568 CustomShaderUniforms {
571 resolution: [self.texture_width as f32, self.texture_height as f32],
572 time,
573 time_delta,
574 mouse,
575 date,
576 opacity: effective_opacity,
577 text_opacity: if self.keep_text_opaque || !apply_opacity {
580 1.0
581 } else {
582 self.window_opacity
583 },
584 full_content_mode: if self.full_content_mode { 1.0 } else { 0.0 },
585 frame: self.frame_count as f32,
586 frame_rate: self.current_frame_rate,
587 resolution_z: 1.0,
588 brightness: self.brightness,
589 key_press_time: self.key_press_time,
590 current_cursor: [
591 curr_x,
592 curr_y,
593 self.cursor_width_for_style(self.current_cursor_style, self.scale_factor),
594 self.cursor_height_for_style(self.current_cursor_style, self.scale_factor),
595 ],
596 previous_cursor: [
597 prev_x,
598 prev_y,
599 self.cursor_width_for_style(self.previous_cursor_style, self.scale_factor),
600 self.cursor_height_for_style(self.previous_cursor_style, self.scale_factor),
601 ],
602 current_cursor_color: [
603 self.current_cursor_color[0],
604 self.current_cursor_color[1],
605 self.current_cursor_color[2],
606 self.current_cursor_color[3] * self.current_cursor_opacity,
607 ],
608 previous_cursor_color: [
609 self.previous_cursor_color[0],
610 self.previous_cursor_color[1],
611 self.previous_cursor_color[2],
612 self.previous_cursor_color[3] * self.previous_cursor_opacity,
613 ],
614 cursor_change_time: self.cursor_change_time,
615 cursor_trail_duration: self.cursor_trail_duration,
616 cursor_glow_radius: self.cursor_glow_radius,
617 cursor_glow_intensity: self.cursor_glow_intensity,
618 cursor_shader_color: self.cursor_shader_color,
619 channel0_resolution: self.effective_channel0_resolution(),
620 channel1_resolution: self.channel_textures[1].resolution(),
621 channel2_resolution: self.channel_textures[2].resolution(),
622 channel3_resolution: self.channel_textures[3].resolution(),
623 channel4_resolution: [
624 self.texture_width as f32,
625 self.texture_height as f32,
626 1.0,
627 0.0,
628 ],
629 cubemap_resolution: self.cubemap.resolution(),
630 background_color: self.background_color,
631 progress: self.progress_data,
632 }
633 }
634
635 fn calculate_date() -> [f32; 4] {
637 use std::time::{SystemTime, UNIX_EPOCH};
638 let now_sys = SystemTime::now();
639 let since_epoch = now_sys.duration_since(UNIX_EPOCH).unwrap_or_default();
640 let secs = since_epoch.as_secs();
641
642 let days_since_epoch = secs / 86400;
643 let secs_today = (secs % 86400) as f32;
644
645 let mut year = 1970i32;
646 let mut remaining_days = days_since_epoch as i32;
647
648 loop {
649 let days_in_year = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
650 366
651 } else {
652 365
653 };
654 if remaining_days < days_in_year {
655 break;
656 }
657 remaining_days -= days_in_year;
658 year += 1;
659 }
660
661 let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
662 let days_in_months: [i32; 12] = if is_leap {
663 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
664 } else {
665 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
666 };
667
668 let mut month = 0i32;
669 for (i, &days) in days_in_months.iter().enumerate() {
670 if remaining_days < days {
671 month = i as i32;
672 break;
673 }
674 remaining_days -= days;
675 }
676
677 let day = remaining_days + 1;
678 [year as f32, month as f32, day as f32, secs_today]
679 }
680
681 #[allow(dead_code)]
683 pub fn animation_enabled(&self) -> bool {
684 self.animation_enabled
685 }
686
687 #[allow(dead_code)]
689 pub fn set_animation_enabled(&mut self, enabled: bool) {
690 self.animation_enabled = enabled;
691 if enabled {
692 self.start_time = Instant::now();
693 }
694 }
695
696 pub fn set_animation_speed(&mut self, speed: f32) {
698 self.animation_speed = speed.max(0.0);
699 }
700
701 pub fn set_opacity(&mut self, opacity: f32) {
703 self.window_opacity = opacity.clamp(0.0, 1.0);
704 }
705
706 pub fn set_brightness(&mut self, brightness: f32) {
708 self.brightness = brightness.clamp(0.05, 1.0);
709 }
710
711 pub fn set_full_content_mode(&mut self, enabled: bool) {
713 self.full_content_mode = enabled;
714 }
715
716 #[allow(dead_code)]
718 pub fn full_content_mode(&self) -> bool {
719 self.full_content_mode
720 }
721
722 pub fn set_keep_text_opaque(&mut self, keep_opaque: bool) {
725 self.keep_text_opaque = keep_opaque;
726 }
727
728 pub fn set_mouse_position(&mut self, x: f32, y: f32) {
730 self.mouse_position = [x, y];
731 }
732
733 pub fn set_mouse_button(&mut self, pressed: bool, x: f32, y: f32) {
735 self.mouse_button_down = pressed;
736 if pressed {
737 self.mouse_click_position = [x, y];
738 }
739 }
740
741 pub fn update_key_press(&mut self) {
746 self.key_press_time = if self.animation_enabled {
747 self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
748 } else {
749 0.0
750 };
751 log::trace!("Key pressed at shader time={:.3}", self.key_press_time);
752 }
753
754 #[allow(dead_code)]
756 pub fn update_channel_texture(
757 &mut self,
758 device: &Device,
759 queue: &Queue,
760 channel: u8,
761 path: Option<&std::path::Path>,
762 ) -> Result<()> {
763 if !(1..=4).contains(&channel) {
764 anyhow::bail!("Invalid channel index: {} (must be 1-4)", channel);
765 }
766
767 let index = (channel - 1) as usize;
768
769 let new_texture = match path {
770 Some(p) => ChannelTexture::from_file(device, queue, p)?,
771 None => ChannelTexture::placeholder(device, queue),
772 };
773
774 self.channel_textures[index] = new_texture;
775
776 self.recreate_bind_group(device);
778
779 log::info!(
780 "Updated iChannel{} texture: {}",
781 channel,
782 path.map(|p| p.display().to_string())
783 .unwrap_or_else(|| "placeholder".to_string())
784 );
785
786 Ok(())
787 }
788
789 #[allow(dead_code)]
791 pub fn update_cubemap(
792 &mut self,
793 device: &Device,
794 queue: &Queue,
795 path: Option<&std::path::Path>,
796 ) -> Result<()> {
797 let new_cubemap = match path {
798 Some(p) => CubemapTexture::from_prefix(device, queue, p)?,
799 None => CubemapTexture::placeholder(device, queue),
800 };
801
802 self.cubemap = new_cubemap;
803
804 self.recreate_bind_group(device);
806
807 log::info!(
808 "Updated cubemap texture: {}",
809 path.map(|p| p.display().to_string())
810 .unwrap_or_else(|| "placeholder".to_string())
811 );
812
813 Ok(())
814 }
815
816 #[allow(dead_code)]
824 pub fn set_use_background_as_channel0(&mut self, use_background: bool) {
825 if self.use_background_as_channel0 != use_background {
826 self.use_background_as_channel0 = use_background;
827 log::info!("use_background_as_channel0 set to {}", use_background);
828 }
829 }
830
831 #[allow(dead_code)]
833 pub fn use_background_as_channel0(&self) -> bool {
834 self.use_background_as_channel0
835 }
836
837 pub fn set_background_texture(&mut self, device: &Device, texture: Option<ChannelTexture>) {
849 self.background_channel_texture = texture;
850
851 if self.use_background_as_channel0 {
854 self.recreate_bind_group(device);
855 }
856 }
857
858 pub fn set_background_color(&mut self, color: [f32; 3], active: bool) {
867 self.background_color = [color[0], color[1], color[2], if active { 1.0 } else { 0.0 }];
868 }
869
870 pub fn update_progress(&mut self, state: f32, percent: f32, is_active: f32, active_count: f32) {
878 self.progress_data = [state, percent, is_active, active_count];
879 }
880
881 fn channel0_has_real_texture(&self) -> bool {
883 let ch0 = &self.channel_textures[0];
884 ch0.width > 1 || ch0.height > 1
886 }
887
888 fn effective_channel0_resolution(&self) -> [f32; 4] {
894 if self.use_background_as_channel0 {
895 self.background_channel_texture
896 .as_ref()
897 .map(|t| t.resolution())
898 .unwrap_or_else(|| self.channel_textures[0].resolution())
899 } else {
900 self.channel_textures[0].resolution()
901 }
902 }
903
904 fn recreate_bind_group(&mut self, device: &Device) {
916 let channel0_texture = if self.use_background_as_channel0 {
918 self.background_channel_texture
920 .as_ref()
921 .unwrap_or(&self.channel_textures[0])
922 } else if self.channel0_has_real_texture() {
923 &self.channel_textures[0]
925 } else {
926 &self.channel_textures[0]
928 };
929
930 let effective_channels = [
932 channel0_texture,
933 &self.channel_textures[1],
934 &self.channel_textures[2],
935 &self.channel_textures[3],
936 ];
937
938 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
939 label: Some("Custom Shader Bind Group"),
940 layout: &self.bind_group_layout,
941 entries: &[
942 wgpu::BindGroupEntry {
943 binding: 0,
944 resource: self.uniform_buffer.as_entire_binding(),
945 },
946 wgpu::BindGroupEntry {
948 binding: 1,
949 resource: wgpu::BindingResource::TextureView(&effective_channels[0].view),
950 },
951 wgpu::BindGroupEntry {
952 binding: 2,
953 resource: wgpu::BindingResource::Sampler(&effective_channels[0].sampler),
954 },
955 wgpu::BindGroupEntry {
957 binding: 3,
958 resource: wgpu::BindingResource::TextureView(&effective_channels[1].view),
959 },
960 wgpu::BindGroupEntry {
961 binding: 4,
962 resource: wgpu::BindingResource::Sampler(&effective_channels[1].sampler),
963 },
964 wgpu::BindGroupEntry {
966 binding: 5,
967 resource: wgpu::BindingResource::TextureView(&effective_channels[2].view),
968 },
969 wgpu::BindGroupEntry {
970 binding: 6,
971 resource: wgpu::BindingResource::Sampler(&effective_channels[2].sampler),
972 },
973 wgpu::BindGroupEntry {
975 binding: 7,
976 resource: wgpu::BindingResource::TextureView(&effective_channels[3].view),
977 },
978 wgpu::BindGroupEntry {
979 binding: 8,
980 resource: wgpu::BindingResource::Sampler(&effective_channels[3].sampler),
981 },
982 wgpu::BindGroupEntry {
984 binding: 9,
985 resource: wgpu::BindingResource::TextureView(&self.intermediate_texture_view),
986 },
987 wgpu::BindGroupEntry {
988 binding: 10,
989 resource: wgpu::BindingResource::Sampler(&self.sampler),
990 },
991 wgpu::BindGroupEntry {
993 binding: 11,
994 resource: wgpu::BindingResource::TextureView(&self.cubemap.view),
995 },
996 wgpu::BindGroupEntry {
997 binding: 12,
998 resource: wgpu::BindingResource::Sampler(&self.cubemap.sampler),
999 },
1000 ],
1001 });
1002 }
1003
1004 pub fn update_use_background_as_channel0(&mut self, device: &Device, use_background: bool) {
1008 if self.use_background_as_channel0 != use_background {
1009 self.use_background_as_channel0 = use_background;
1010 self.recreate_bind_group(device);
1011 log::info!("use_background_as_channel0 toggled to {}", use_background);
1012 }
1013 }
1014
1015 pub fn reload_from_source(&mut self, device: &Device, source: &str, name: &str) -> Result<()> {
1017 let wgsl_source = transpile_glsl_to_wgsl_source(source, name)?;
1018
1019 log::info!(
1020 "Reloading custom shader from source ({} bytes GLSL -> {} bytes WGSL)",
1021 source.len(),
1022 wgsl_source.len()
1023 );
1024 log::debug!("Generated WGSL:\n{}", wgsl_source);
1025
1026 let module = naga::front::wgsl::parse_str(&wgsl_source)
1028 .context("Custom shader WGSL parse failed")?;
1029 let _info = naga::valid::Validator::new(
1030 naga::valid::ValidationFlags::all(),
1031 naga::valid::Capabilities::empty(),
1032 )
1033 .validate(&module)
1034 .context("Custom shader WGSL validation failed")?;
1035
1036 let shader_module = device.create_shader_module(ShaderModuleDescriptor {
1037 label: Some("Custom Shader Module (reloaded)"),
1038 source: ShaderSource::Wgsl(wgsl_source.into()),
1039 });
1040
1041 self.pipeline = create_render_pipeline(
1042 device,
1043 &shader_module,
1044 &self.bind_group_layout,
1045 self.surface_format,
1046 Some("Custom Shader Pipeline (reloaded)"),
1047 );
1048
1049 self.start_time = Instant::now();
1050
1051 log::info!("Custom shader reloaded successfully from source");
1052 Ok(())
1053 }
1054
1055 pub fn set_content_inset_right(&mut self, inset: f32) {
1060 self.content_inset_right = inset;
1061 }
1062}