ratatui_wgpu/
shaders.rs

1use std::{
2    mem::size_of,
3    num::NonZeroU64,
4};
5
6use web_time::Instant;
7use wgpu::{
8    self,
9    include_wgsl,
10    AddressMode,
11    BindGroup,
12    BindGroupDescriptor,
13    BindGroupEntry,
14    BindGroupLayout,
15    BindGroupLayoutDescriptor,
16    BindGroupLayoutEntry,
17    BindingResource,
18    BindingType,
19    Buffer,
20    BufferBindingType,
21    BufferDescriptor,
22    BufferUsages,
23    Color,
24    ColorTargetState,
25    ColorWrites,
26    CommandEncoder,
27    Device,
28    Extent3d,
29    FilterMode,
30    FragmentState,
31    LoadOp,
32    MultisampleState,
33    Operations,
34    PipelineCompilationOptions,
35    PipelineLayoutDescriptor,
36    PrimitiveState,
37    PrimitiveTopology,
38    Queue,
39    RenderBundle,
40    RenderBundleDescriptor,
41    RenderBundleEncoderDescriptor,
42    RenderPassColorAttachment,
43    RenderPassDescriptor,
44    RenderPipeline,
45    RenderPipelineDescriptor,
46    Sampler,
47    SamplerBindingType,
48    SamplerDescriptor,
49    ShaderStages,
50    StoreOp,
51    SurfaceConfiguration,
52    Texture,
53    TextureDescriptor,
54    TextureDimension,
55    TextureFormat,
56    TextureSampleType,
57    TextureUsages,
58    TextureView,
59    TextureViewDescriptor,
60    TextureViewDimension,
61    VertexState,
62};
63
64use crate::backend::PostProcessor;
65
66#[repr(C)]
67#[derive(bytemuck::Pod, bytemuck::Zeroable, Debug, Clone, Copy)]
68struct Uniforms {
69    screen_size: [f32; 2],
70    preserve_aspect: u32,
71    use_srgb: u32,
72}
73
74/// The default post-processor. Used when you don't want to perform any custom
75/// shading on the output. This just blits the composited text to the surface.
76/// This will stretch characters if the render area size falls between multiples
77/// of the character size. Use `AspectPreservingDefaultPostProcessor` if you
78/// don't want this behavior.
79pub struct DefaultPostProcessor<const PRESERVE_ASPECT: bool = false> {
80    uniforms: Buffer,
81    bindings: BindGroupLayout,
82    sampler: Sampler,
83    pipeline: RenderPipeline,
84
85    blitter: RenderBundle,
86}
87
88/// A default post-processor which preserves the aspect ratio of characters when
89/// the render area size falls in between multiples of the character size.
90pub type AspectPreservingDefaultPostProcessor = DefaultPostProcessor<true>;
91
92impl<const PRESERVE_ASPECT: bool> PostProcessor for DefaultPostProcessor<PRESERVE_ASPECT> {
93    type UserData = ();
94
95    fn compile(
96        device: &Device,
97        text_view: &TextureView,
98        surface_config: &SurfaceConfiguration,
99        _user_data: Self::UserData,
100    ) -> Self {
101        let uniforms = device.create_buffer(&BufferDescriptor {
102            label: Some("Text Blit Uniforms"),
103            size: size_of::<Uniforms>() as u64,
104            usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM,
105            mapped_at_creation: false,
106        });
107
108        let sampler = device.create_sampler(&SamplerDescriptor {
109            address_mode_u: AddressMode::ClampToEdge,
110            address_mode_v: AddressMode::ClampToEdge,
111            address_mode_w: AddressMode::ClampToEdge,
112            mag_filter: FilterMode::Nearest,
113            min_filter: FilterMode::Nearest,
114            mipmap_filter: FilterMode::Nearest,
115            ..Default::default()
116        });
117
118        let layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
119            label: Some("Text Blit Bindings Layout"),
120            entries: &[
121                BindGroupLayoutEntry {
122                    binding: 0,
123                    visibility: ShaderStages::FRAGMENT,
124                    ty: BindingType::Texture {
125                        sample_type: TextureSampleType::Float { filterable: true },
126                        view_dimension: TextureViewDimension::D2,
127                        multisampled: false,
128                    },
129                    count: None,
130                },
131                BindGroupLayoutEntry {
132                    binding: 1,
133                    visibility: ShaderStages::FRAGMENT,
134                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
135                    count: None,
136                },
137                BindGroupLayoutEntry {
138                    binding: 2,
139                    visibility: ShaderStages::FRAGMENT,
140                    ty: BindingType::Buffer {
141                        ty: BufferBindingType::Uniform,
142                        has_dynamic_offset: false,
143                        min_binding_size: NonZeroU64::new(size_of::<Uniforms>() as u64),
144                    },
145                    count: None,
146                },
147            ],
148        });
149
150        let shader = device.create_shader_module(include_wgsl!("shaders/blit.wgsl"));
151
152        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
153            label: Some("Text Blit Layout"),
154            bind_group_layouts: &[&layout],
155            push_constant_ranges: &[],
156        });
157
158        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
159            label: Some("Text Blitter Pipeline"),
160            layout: Some(&pipeline_layout),
161            vertex: VertexState {
162                module: &shader,
163                entry_point: Some("vs_main"),
164                compilation_options: PipelineCompilationOptions::default(),
165                buffers: &[],
166            },
167            primitive: PrimitiveState {
168                topology: PrimitiveTopology::TriangleStrip,
169                ..Default::default()
170            },
171            depth_stencil: None,
172            multisample: MultisampleState::default(),
173            fragment: Some(FragmentState {
174                module: &shader,
175                entry_point: Some("fs_main"),
176                compilation_options: PipelineCompilationOptions::default(),
177                targets: &[Some(ColorTargetState {
178                    format: surface_config.format,
179                    blend: None,
180                    write_mask: ColorWrites::ALL,
181                })],
182            }),
183            multiview: None,
184            cache: None,
185        });
186
187        let blitter = build_blitter(
188            device,
189            &layout,
190            text_view,
191            &sampler,
192            &uniforms,
193            surface_config,
194            &pipeline,
195        );
196
197        Self {
198            uniforms,
199            bindings: layout,
200            sampler,
201            pipeline,
202            blitter,
203        }
204    }
205
206    fn resize(
207        &mut self,
208        device: &Device,
209        text_view: &TextureView,
210        surface_config: &SurfaceConfiguration,
211    ) {
212        self.blitter = build_blitter(
213            device,
214            &self.bindings,
215            text_view,
216            &self.sampler,
217            &self.uniforms,
218            surface_config,
219            &self.pipeline,
220        );
221    }
222
223    fn process(
224        &mut self,
225        encoder: &mut CommandEncoder,
226        queue: &Queue,
227        _text_view: &TextureView,
228        surface_config: &SurfaceConfiguration,
229        surface_view: &TextureView,
230    ) {
231        {
232            let mut uniforms = queue
233                .write_buffer_with(
234                    &self.uniforms,
235                    0,
236                    NonZeroU64::new(size_of::<Uniforms>() as u64).unwrap(),
237                )
238                .unwrap();
239            uniforms.copy_from_slice(bytemuck::bytes_of(&Uniforms {
240                screen_size: [surface_config.width as f32, surface_config.height as f32],
241                preserve_aspect: u32::from(PRESERVE_ASPECT),
242                use_srgb: u32::from(surface_config.format.is_srgb()),
243            }));
244        }
245
246        let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
247            label: Some("Text Blit Pass"),
248            color_attachments: &[Some(RenderPassColorAttachment {
249                view: surface_view,
250                resolve_target: None,
251                ops: Operations {
252                    load: LoadOp::Clear(Color::TRANSPARENT),
253                    store: StoreOp::Store,
254                },
255                depth_slice: None,
256            })],
257            ..Default::default()
258        });
259
260        pass.execute_bundles(Some(&self.blitter));
261    }
262}
263
264fn build_blitter(
265    device: &Device,
266    layout: &BindGroupLayout,
267    text_view: &TextureView,
268    sampler: &Sampler,
269    uniforms: &Buffer,
270    surface_config: &SurfaceConfiguration,
271    pipeline: &RenderPipeline,
272) -> RenderBundle {
273    let bindings = device.create_bind_group(&BindGroupDescriptor {
274        label: Some("Text Blit Bindings"),
275        layout,
276        entries: &[
277            BindGroupEntry {
278                binding: 0,
279                resource: BindingResource::TextureView(text_view),
280            },
281            BindGroupEntry {
282                binding: 1,
283                resource: BindingResource::Sampler(sampler),
284            },
285            BindGroupEntry {
286                binding: 2,
287                resource: uniforms.as_entire_binding(),
288            },
289        ],
290    });
291
292    let mut encoder = device.create_render_bundle_encoder(&RenderBundleEncoderDescriptor {
293        label: Some("Text Blit Pass Encoder"),
294        color_formats: &[Some(surface_config.format)],
295        depth_stencil: None,
296        sample_count: 1,
297        multiview: None,
298    });
299
300    encoder.set_pipeline(pipeline);
301
302    encoder.set_bind_group(0, &bindings, &[]);
303    encoder.draw(0..3, 0..1);
304
305    encoder.finish(&RenderBundleDescriptor {
306        label: Some("Text Blit Pass Bundle"),
307    })
308}
309
310#[repr(C)]
311#[derive(bytemuck::Pod, bytemuck::Zeroable, Debug, Clone, Copy)]
312struct CrtUniforms {
313    modulate_crt: [f32; 3],
314    // All vec3s are padded like vec 4s
315    _pad0: f32,
316    resolution: [f32; 2],
317    brightness: f32,
318    modulate_accumulate: f32,
319    modulate_blend: f32,
320    slow_fade: i32,
321    curve_factor: f32,
322    ghost_factor: f32,
323    scanline_factor: f32,
324    corner_radius: f32,
325    mask_type: f32,
326    mask_strength: f32,
327    use_srgb: i32,
328    milliseconds: u32,
329    _pad1: [f32; 2],
330}
331
332/// Settings for the CRT post-processor.
333///
334/// See struct members for more information on each setting.
335#[derive(Debug, Clone)]
336pub struct CrtSettings {
337    /// How much to increase/reduce the red channel of the CRT effect.
338    /// A good range of values is 0.1 to 2.0.
339    /// Defaults to 1.0.
340    pub modulate_r: f32,
341    /// How much to increase/reduce the green channel of the CRT effect.
342    /// A good range of values is 0.1 to 2.0.
343    /// Defaults to 1.0.
344    pub modulate_g: f32,
345    /// How much to increase/reduce the blue channel of the CRT effect.
346    /// A good range of values is 0.1 to 2.0.
347    /// Defaults to 1.0.
348    pub modulate_b: f32,
349    /// The brightness of the CRT effect.
350    /// A good range of values is 0.0 to 0.2.
351    /// Defaults to 0.09.
352    pub brightness: f32,
353    /// How much to curve the screen for the CRT effect.
354    /// A good range of values is 0.0 to 2.0.
355    /// Defaults to 1.0.
356    pub curve_factor: f32,
357    /// How much "ghosting" to apply to the CRT effect.
358    /// A good range of values is 0.0 to 1.0.
359    /// Defaults to 0.15.
360    pub ghost_factor: f32,
361    /// How strongly to apply the scanline effect.
362    /// A good range of values is 0.0 to 2.0.
363    /// Defaults to 0.4.
364    pub scanline_factor: f32,
365    /// The radius of the corner clipping.
366    /// A good range of values is 0.0 to 500.0.
367    /// Defaults to 210.0.
368    pub corner_radius_factor: f32,
369    /// The type of mask to apply to the CRT effect.
370    /// 1.0 - TV style mask
371    /// 2.0 - Apeture-grille style mask
372    /// 3.0 - VGA (stretched)
373    /// 4.0 - VGA (no stretch)
374    /// Other values will disable the mask.
375    /// Defaults to 3.0.
376    pub mask_type: f32,
377    /// How strongly to apply the mask to the CRT effect.
378    /// A good range of values is 0.0 to 1.0.
379    /// Defaults to 0.2.
380    pub mask_strength: f32,
381    /// How much to fade between frames for the CRT effect. A value of 1.0 will
382    /// result in ghosting from previous frames for animations/screen
383    /// transitions.
384    /// Defaults to 0.0.
385    pub slow_fade: f32,
386}
387
388impl Default for CrtSettings {
389    fn default() -> Self {
390        Self {
391            modulate_r: 1.0,
392            modulate_g: 1.0,
393            modulate_b: 1.0,
394            brightness: 0.09,
395            curve_factor: 1.0,
396            ghost_factor: 0.15,
397            scanline_factor: 0.4,
398            corner_radius_factor: 210.0,
399            mask_type: 3.0,
400            mask_strength: 0.2,
401            slow_fade: 0.0,
402        }
403    }
404}
405
406/// A post-processor which applies a CRT effect to the output.
407pub struct CrtPostProcessor {
408    _sampler: Sampler,
409
410    _crt_uniforms_buffer: Buffer,
411    crt_pass: RenderBundle,
412
413    blur_x_uniforms: Buffer,
414    blur_y_uniforms: Buffer,
415
416    blur_x_dest: TextureView,
417    blur_x_pass: RenderBundle,
418
419    blur_y_dest: TextureView,
420    blur_y_pass: RenderBundle,
421
422    accumulate_texture_in: Texture,
423    accumulate_texture_out: Texture,
424    accumulate_view_out: TextureView,
425
426    width: u32,
427    height: u32,
428    timer: Instant,
429
430    settings: CrtSettings,
431}
432
433impl PostProcessor for CrtPostProcessor {
434    type UserData = CrtSettings;
435
436    fn compile(
437        device: &Device,
438        text_view: &TextureView,
439        surface_config: &SurfaceConfiguration,
440        user_data: Self::UserData,
441    ) -> Self {
442        let drawable_width = surface_config.width;
443        #[cfg(not(target_arch = "wasm32"))]
444        let drawable_height = surface_config.height - 1;
445        #[cfg(target_arch = "wasm32")]
446        let drawable_height = surface_config.height;
447
448        let sampler = device.create_sampler(&SamplerDescriptor {
449            address_mode_u: AddressMode::ClampToEdge,
450            address_mode_v: AddressMode::ClampToEdge,
451            address_mode_w: AddressMode::ClampToEdge,
452            mag_filter: FilterMode::Nearest,
453            min_filter: FilterMode::Nearest,
454            mipmap_filter: FilterMode::Nearest,
455            ..Default::default()
456        });
457
458        let texture_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
459            label: Some("Texture Sourced Layout"),
460            entries: &[
461                BindGroupLayoutEntry {
462                    binding: 0,
463                    visibility: ShaderStages::FRAGMENT,
464                    ty: BindingType::Texture {
465                        sample_type: TextureSampleType::Float { filterable: true },
466                        view_dimension: TextureViewDimension::D2,
467                        multisampled: false,
468                    },
469                    count: None,
470                },
471                BindGroupLayoutEntry {
472                    binding: 1,
473                    visibility: ShaderStages::FRAGMENT,
474                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
475                    count: None,
476                },
477            ],
478        });
479
480        let text_out_as_in_binding = device.create_bind_group(&BindGroupDescriptor {
481            label: Some("Text Compositor Output"),
482            layout: &texture_layout,
483            entries: &[
484                BindGroupEntry {
485                    binding: 0,
486                    resource: BindingResource::TextureView(text_view),
487                },
488                BindGroupEntry {
489                    binding: 1,
490                    resource: BindingResource::Sampler(&sampler),
491                },
492            ],
493        });
494
495        let accumulate_texture_in = device.create_texture(&TextureDescriptor {
496            label: Some("Accumulate Out A"),
497            size: Extent3d {
498                width: drawable_width,
499                height: drawable_height,
500                depth_or_array_layers: 1,
501            },
502            mip_level_count: 1,
503            sample_count: 1,
504            dimension: TextureDimension::D2,
505            format: TextureFormat::Rgba8Unorm,
506            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
507            view_formats: &[],
508        });
509
510        let accumulate_view_in =
511            accumulate_texture_in.create_view(&TextureViewDescriptor::default());
512
513        let accumulate_in_binding = device.create_bind_group(&BindGroupDescriptor {
514            label: Some("Accumulate Input"),
515            layout: &texture_layout,
516            entries: &[
517                BindGroupEntry {
518                    binding: 0,
519                    resource: BindingResource::TextureView(&accumulate_view_in),
520                },
521                BindGroupEntry {
522                    binding: 1,
523                    resource: BindingResource::Sampler(&sampler),
524                },
525            ],
526        });
527
528        let accumulate_texture_out = device.create_texture(&TextureDescriptor {
529            label: Some("Accumulate Out B"),
530            size: Extent3d {
531                width: drawable_width,
532                height: surface_config.height,
533                depth_or_array_layers: 1,
534            },
535            mip_level_count: 1,
536            sample_count: 1,
537            dimension: TextureDimension::D2,
538            format: TextureFormat::Rgba8Unorm,
539            usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
540            view_formats: &[],
541        });
542
543        let accumulate_view_out =
544            accumulate_texture_out.create_view(&TextureViewDescriptor::default());
545
546        let crt_uniforms_buffer = device.create_buffer(&BufferDescriptor {
547            label: Some("CRT Uniforms buffer"),
548            size: size_of::<CrtUniforms>() as u64,
549            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
550            mapped_at_creation: false,
551        });
552
553        let (crt_pipeline, crt_fs_uniforms) = build_crt(
554            device,
555            surface_config,
556            &texture_layout,
557            &crt_uniforms_buffer,
558        );
559
560        let blur_x_dest = device.create_texture(&TextureDescriptor {
561            label: Some("Blur x pass"),
562            size: Extent3d {
563                width: drawable_width,
564                height: drawable_height,
565                depth_or_array_layers: 1,
566            },
567            mip_level_count: 1,
568            sample_count: 1,
569            dimension: TextureDimension::D2,
570            format: TextureFormat::Rgba8Unorm,
571            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
572            view_formats: &[],
573        });
574
575        let blur_x_dest = blur_x_dest.create_view(&TextureViewDescriptor::default());
576
577        let blur_y_dest = device.create_texture(&TextureDescriptor {
578            label: Some("Blur y pass"),
579            size: Extent3d {
580                width: drawable_width,
581                height: drawable_height,
582                depth_or_array_layers: 1,
583            },
584            mip_level_count: 1,
585            sample_count: 1,
586            dimension: TextureDimension::D2,
587            format: TextureFormat::Rgba8Unorm,
588            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
589            view_formats: &[],
590        });
591
592        let blur_y_dest = blur_y_dest.create_view(&TextureViewDescriptor::default());
593
594        let blur_out_as_in_binding = device.create_bind_group(&BindGroupDescriptor {
595            label: Some("Blur output binding"),
596            layout: &texture_layout,
597            entries: &[
598                BindGroupEntry {
599                    binding: 0,
600                    resource: BindingResource::TextureView(&blur_y_dest),
601                },
602                BindGroupEntry {
603                    binding: 1,
604                    resource: BindingResource::Sampler(&sampler),
605                },
606            ],
607        });
608
609        let blur_x_uniforms = device.create_buffer(&BufferDescriptor {
610            label: Some("Blur buffer"),
611            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
612            size: size_of::<[f32; 4]>() as u64,
613            mapped_at_creation: false,
614        });
615
616        let blur_y_uniforms = device.create_buffer(&BufferDescriptor {
617            label: Some("Blur buffer"),
618            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
619            size: size_of::<[f32; 4]>() as u64,
620            mapped_at_creation: false,
621        });
622
623        let (blur_pipeline, blur_layout) = build_blur(device);
624
625        let blur_x_pass = {
626            let mut encoder = device.create_render_bundle_encoder(&RenderBundleEncoderDescriptor {
627                label: Some("Blur x text pass"),
628                color_formats: &[Some(TextureFormat::Rgba8Unorm)],
629                depth_stencil: None,
630                sample_count: 1,
631                multiview: None,
632            });
633
634            encoder.set_pipeline(&blur_pipeline);
635
636            let blur_bindings = device.create_bind_group(&BindGroupDescriptor {
637                label: Some("Blur x text bindings"),
638                layout: &blur_layout,
639                entries: &[
640                    BindGroupEntry {
641                        binding: 0,
642                        resource: BindingResource::TextureView(text_view),
643                    },
644                    BindGroupEntry {
645                        binding: 1,
646                        resource: BindingResource::Sampler(&sampler),
647                    },
648                    BindGroupEntry {
649                        binding: 2,
650                        resource: blur_x_uniforms.as_entire_binding(),
651                    },
652                ],
653            });
654            encoder.set_bind_group(0, &blur_bindings, &[]);
655            encoder.draw(0..3, 0..1);
656
657            encoder.finish(&RenderBundleDescriptor {
658                label: Some("Blur x text render bundle"),
659            })
660        };
661
662        let blur_y_pass = {
663            let mut encoder = device.create_render_bundle_encoder(&RenderBundleEncoderDescriptor {
664                label: Some("Blur y pass"),
665                color_formats: &[Some(TextureFormat::Rgba8Unorm)],
666                depth_stencil: None,
667                sample_count: 1,
668                multiview: None,
669            });
670
671            encoder.set_pipeline(&blur_pipeline);
672
673            let blur_bindings = device.create_bind_group(&BindGroupDescriptor {
674                label: Some("Blur y bindings"),
675                layout: &blur_layout,
676                entries: &[
677                    BindGroupEntry {
678                        binding: 0,
679                        resource: BindingResource::TextureView(&blur_x_dest),
680                    },
681                    BindGroupEntry {
682                        binding: 1,
683                        resource: BindingResource::Sampler(&sampler),
684                    },
685                    BindGroupEntry {
686                        binding: 2,
687                        resource: blur_y_uniforms.as_entire_binding(),
688                    },
689                ],
690            });
691            encoder.set_bind_group(0, &blur_bindings, &[]);
692            encoder.draw(0..3, 0..1);
693
694            encoder.finish(&RenderBundleDescriptor {
695                label: Some("Blur y render bundle"),
696            })
697        };
698
699        let crt_pass = {
700            let mut encoder = device.create_render_bundle_encoder(&RenderBundleEncoderDescriptor {
701                label: Some("CRT Render Bundle Encoder"),
702                color_formats: &[Some(surface_config.format), Some(TextureFormat::Rgba8Unorm)],
703                depth_stencil: None,
704                sample_count: 1,
705                multiview: None,
706            });
707
708            encoder.set_pipeline(&crt_pipeline);
709
710            encoder.set_bind_group(0, &text_out_as_in_binding, &[]);
711            encoder.set_bind_group(1, &accumulate_in_binding, &[]);
712            encoder.set_bind_group(2, &blur_out_as_in_binding, &[]);
713            encoder.set_bind_group(3, &crt_fs_uniforms, &[]);
714            encoder.draw(0..3, 0..1);
715
716            encoder.finish(&RenderBundleDescriptor {
717                label: Some("CRT Render Bundle"),
718            })
719        };
720
721        Self {
722            _sampler: sampler,
723            _crt_uniforms_buffer: crt_uniforms_buffer,
724            crt_pass,
725            blur_x_uniforms,
726            blur_y_uniforms,
727            blur_x_dest,
728            blur_x_pass,
729            blur_y_dest,
730            blur_y_pass,
731            accumulate_texture_in,
732            accumulate_texture_out,
733            accumulate_view_out,
734            width: drawable_width,
735            height: drawable_height,
736            timer: Instant::now(),
737            settings: user_data,
738        }
739    }
740
741    fn resize(
742        &mut self,
743        device: &Device,
744        text_view: &TextureView,
745        surface_config: &SurfaceConfiguration,
746    ) {
747        let settings = self.settings.clone();
748        let timer = self.timer;
749
750        *self = Self::compile(device, text_view, surface_config, settings);
751
752        self.timer = timer;
753    }
754
755    fn process(
756        &mut self,
757        encoder: &mut CommandEncoder,
758        queue: &Queue,
759        _text_view: &TextureView,
760        surface_config: &SurfaceConfiguration,
761        surface_view: &TextureView,
762    ) {
763        {
764            let mut uniforms = queue
765                .write_buffer_with(
766                    &self.blur_x_uniforms,
767                    0,
768                    NonZeroU64::new(size_of::<[f32; 2]>() as u64).unwrap(),
769                )
770                .unwrap();
771            uniforms.copy_from_slice(bytemuck::cast_slice(&[1f32 / self.width as f32, 0.0]));
772        }
773
774        {
775            let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
776                label: Some("Blur x pass"),
777                color_attachments: &[Some(RenderPassColorAttachment {
778                    view: &self.blur_x_dest,
779                    resolve_target: None,
780                    ops: Operations {
781                        load: LoadOp::Clear(Color::TRANSPARENT),
782                        store: StoreOp::Store,
783                    },
784                    depth_slice: None,
785                })],
786                ..Default::default()
787            });
788
789            render_pass.execute_bundles(Some(&self.blur_x_pass));
790        }
791
792        {
793            let mut uniforms = queue
794                .write_buffer_with(
795                    &self.blur_y_uniforms,
796                    0,
797                    NonZeroU64::new(size_of::<[f32; 2]>() as u64).unwrap(),
798                )
799                .unwrap();
800            uniforms.copy_from_slice(bytemuck::cast_slice(&[0.0, 1f32 / self.height as f32]));
801        }
802
803        {
804            let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
805                label: Some("Blur y pass"),
806                color_attachments: &[Some(RenderPassColorAttachment {
807                    view: &self.blur_y_dest,
808                    resolve_target: None,
809                    ops: Operations {
810                        load: LoadOp::Clear(Color::TRANSPARENT),
811                        store: StoreOp::Store,
812                    },
813                    depth_slice: None,
814                })],
815                ..Default::default()
816            });
817
818            render_pass.execute_bundles(Some(&self.blur_y_pass));
819        }
820
821        {
822            let mut uniforms = queue
823                .write_buffer_with(
824                    &self._crt_uniforms_buffer,
825                    0,
826                    NonZeroU64::new(size_of::<CrtUniforms>() as u64).unwrap(),
827                )
828                .unwrap();
829
830            uniforms.copy_from_slice(bytemuck::bytes_of(&CrtUniforms {
831                modulate_crt: [
832                    self.settings.modulate_r,
833                    self.settings.modulate_g,
834                    self.settings.modulate_b,
835                ],
836                _pad0: 0.,
837                brightness: self.settings.brightness,
838                modulate_accumulate: 1.,
839                modulate_blend: 1.,
840                slow_fade: i32::from(self.settings.slow_fade == 1.0),
841                resolution: [self.width as f32, self.height as f32],
842                curve_factor: self.settings.curve_factor,
843                ghost_factor: self.settings.ghost_factor,
844                scanline_factor: self.settings.scanline_factor,
845                corner_radius: self.settings.corner_radius_factor,
846                mask_type: self.settings.mask_type,
847                mask_strength: self.settings.mask_strength,
848                use_srgb: i32::from(surface_config.format.is_srgb()),
849                milliseconds: self.timer.elapsed().as_millis() as u32,
850                _pad1: [0.0; 2],
851            }));
852        }
853        self.timer = Instant::now();
854
855        {
856            let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
857                label: Some("CRT Pass"),
858                color_attachments: &[
859                    Some(RenderPassColorAttachment {
860                        view: surface_view,
861                        resolve_target: None,
862                        ops: Operations {
863                            load: LoadOp::Clear(Color::TRANSPARENT),
864                            store: StoreOp::Store,
865                        },
866                        depth_slice: None,
867                    }),
868                    Some(RenderPassColorAttachment {
869                        view: &self.accumulate_view_out,
870                        resolve_target: None,
871                        ops: Operations {
872                            load: LoadOp::Load,
873                            store: StoreOp::Store,
874                        },
875                        depth_slice: None,
876                    }),
877                ],
878                ..Default::default()
879            });
880
881            render_pass.execute_bundles(Some(&self.crt_pass));
882        }
883
884        encoder.copy_texture_to_texture(
885            self.accumulate_texture_out.as_image_copy(),
886            self.accumulate_texture_in.as_image_copy(),
887            Extent3d {
888                width: self.width,
889                height: self.height,
890                depth_or_array_layers: 1,
891            },
892        );
893    }
894
895    fn needs_update(&self) -> bool {
896        self.settings.slow_fade == 1.0
897    }
898}
899
900fn build_blur(device: &Device) -> (RenderPipeline, BindGroupLayout) {
901    let shader = device.create_shader_module(include_wgsl!("shaders/blur.wgsl"));
902
903    let fragment_shader_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
904        label: Some("Blur Fragment Binding Layout"),
905        entries: &[
906            BindGroupLayoutEntry {
907                binding: 0,
908                visibility: ShaderStages::FRAGMENT,
909                ty: BindingType::Texture {
910                    sample_type: TextureSampleType::Float { filterable: true },
911                    view_dimension: TextureViewDimension::D2,
912                    multisampled: false,
913                },
914                count: None,
915            },
916            BindGroupLayoutEntry {
917                binding: 1,
918                visibility: ShaderStages::FRAGMENT,
919                ty: BindingType::Sampler(SamplerBindingType::Filtering),
920                count: None,
921            },
922            BindGroupLayoutEntry {
923                binding: 2,
924                visibility: ShaderStages::FRAGMENT,
925                ty: BindingType::Buffer {
926                    ty: BufferBindingType::Uniform,
927                    has_dynamic_offset: false,
928                    min_binding_size: Some(NonZeroU64::new(size_of::<[f32; 4]>() as u64).unwrap()),
929                },
930                count: None,
931            },
932        ],
933    });
934
935    let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
936        label: Some("Blur Layout"),
937        bind_group_layouts: &[&fragment_shader_layout],
938        push_constant_ranges: &[],
939    });
940
941    let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
942        label: Some("Blur Pipeline"),
943        layout: Some(&pipeline_layout),
944        vertex: VertexState {
945            module: &shader,
946            entry_point: Some("vs_main"),
947            compilation_options: PipelineCompilationOptions::default(),
948            buffers: &[],
949        },
950        primitive: PrimitiveState {
951            topology: PrimitiveTopology::TriangleStrip,
952            ..Default::default()
953        },
954        depth_stencil: None,
955        multisample: MultisampleState::default(),
956        fragment: Some(FragmentState {
957            module: &shader,
958            entry_point: Some("fs_main"),
959            compilation_options: PipelineCompilationOptions::default(),
960            targets: &[Some(ColorTargetState {
961                format: TextureFormat::Rgba8Unorm,
962                blend: None,
963                write_mask: ColorWrites::ALL,
964            })],
965        }),
966        multiview: None,
967        cache: None,
968    });
969
970    (pipeline, fragment_shader_layout)
971}
972
973fn build_crt(
974    device: &Device,
975    config: &SurfaceConfiguration,
976    texture_layout: &BindGroupLayout,
977    crt_uniforms_buffer: &Buffer,
978) -> (RenderPipeline, BindGroup) {
979    let shader = device.create_shader_module(include_wgsl!("shaders/crt.wgsl"));
980
981    let uniforms_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
982        label: Some("CRT Fragment Uniforms Binding Layout"),
983        entries: &[BindGroupLayoutEntry {
984            binding: 0,
985            visibility: ShaderStages::FRAGMENT,
986            ty: BindingType::Buffer {
987                ty: BufferBindingType::Uniform,
988                has_dynamic_offset: false,
989                min_binding_size: Some(NonZeroU64::new(size_of::<CrtUniforms>() as u64).unwrap()),
990            },
991            count: None,
992        }],
993    });
994
995    let fs_uniforms = device.create_bind_group(&BindGroupDescriptor {
996        label: Some("CRT Fragment Uniforms Binding"),
997        layout: &uniforms_layout,
998        entries: &[BindGroupEntry {
999            binding: 0,
1000            resource: crt_uniforms_buffer.as_entire_binding(),
1001        }],
1002    });
1003
1004    let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
1005        label: Some("CRT Layout"),
1006        bind_group_layouts: &[
1007            texture_layout,
1008            texture_layout,
1009            texture_layout,
1010            &uniforms_layout,
1011        ],
1012        push_constant_ranges: &[],
1013    });
1014
1015    let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
1016        label: Some("CRT Pipeline"),
1017        layout: Some(&pipeline_layout),
1018        vertex: VertexState {
1019            module: &shader,
1020            entry_point: Some("vs_main"),
1021            compilation_options: PipelineCompilationOptions::default(),
1022            buffers: &[],
1023        },
1024        primitive: PrimitiveState {
1025            topology: PrimitiveTopology::TriangleStrip,
1026            ..Default::default()
1027        },
1028        depth_stencil: None,
1029        multisample: MultisampleState::default(),
1030        fragment: Some(FragmentState {
1031            module: &shader,
1032            entry_point: Some("fs_main"),
1033            compilation_options: PipelineCompilationOptions::default(),
1034            targets: &[
1035                Some(ColorTargetState {
1036                    format: config.format,
1037                    blend: None,
1038                    write_mask: ColorWrites::ALL,
1039                }),
1040                Some(ColorTargetState {
1041                    format: TextureFormat::Rgba8Unorm,
1042                    blend: None,
1043                    write_mask: ColorWrites::ALL,
1044                }),
1045            ],
1046        }),
1047        multiview: None,
1048        cache: None,
1049    });
1050
1051    (pipeline, fs_uniforms)
1052}