Skip to main content

scenix_post/
lib.rs

1//! GPU post-processing stack for scenix.
2//!
3//! `scenix-post` owns full-screen wgpu passes and does not depend on the
4//! renderer crate. Renderers can feed it a source texture view and final target
5//! view while avoiding a Cargo dependency cycle.
6
7use std::sync::Arc;
8
9use bytemuck::{Pod, Zeroable};
10use scenix_core::{ScenixError, ValidationError};
11use scenix_math::Vec2;
12
13/// Per-frame post-processing context.
14#[derive(Clone, Copy, Debug, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct PostContext {
17    /// Monotonic frame index.
18    pub frame_index: u64,
19    /// Target resolution in pixels.
20    pub resolution: Vec2,
21    /// Output color format.
22    pub color_format: wgpu::TextureFormat,
23}
24
25/// CPU counters for a post-processing dispatch.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct PostStats {
29    /// Number of enabled post passes submitted.
30    pub passes: u32,
31    /// Number of scratch targets resized during this dispatch.
32    pub resized_targets: u32,
33}
34
35/// Bloom highlight extraction and additive glow configuration.
36#[derive(Clone, Copy, Debug, PartialEq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct BloomConfig {
39    /// Luminance threshold where bloom begins.
40    pub threshold: f32,
41    /// Bloom contribution multiplier.
42    pub intensity: f32,
43    /// Approximate blur radius in pixels.
44    pub radius: f32,
45}
46
47impl BloomConfig {
48    /// Returns a clamped configuration.
49    pub fn normalized(self) -> Self {
50        Self {
51            threshold: self.threshold.clamp(0.0, 16.0),
52            intensity: self.intensity.clamp(0.0, 16.0),
53            radius: self.radius.clamp(0.0, 64.0),
54        }
55    }
56}
57
58impl Default for BloomConfig {
59    #[inline]
60    fn default() -> Self {
61        Self {
62            threshold: 1.0,
63            intensity: 0.35,
64            radius: 4.0,
65        }
66    }
67}
68
69/// Screen-space ambient occlusion post configuration.
70#[derive(Clone, Copy, Debug, PartialEq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct SsaoConfig {
73    /// Sampling radius in view-space units.
74    pub radius: f32,
75    /// Occlusion strength.
76    pub intensity: f32,
77    /// Self-occlusion bias.
78    pub bias: f32,
79}
80
81impl SsaoConfig {
82    /// Returns a clamped configuration.
83    pub fn normalized(self) -> Self {
84        Self {
85            radius: self.radius.clamp(0.0, 16.0),
86            intensity: self.intensity.clamp(0.0, 4.0),
87            bias: self.bias.clamp(0.0, 1.0),
88        }
89    }
90}
91
92impl Default for SsaoConfig {
93    #[inline]
94    fn default() -> Self {
95        Self {
96            radius: 0.5,
97            intensity: 1.0,
98            bias: 0.025,
99        }
100    }
101}
102
103/// Tone mapping operator.
104#[derive(Clone, Copy, Debug, Default, PartialEq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub enum ToneMapper {
107    /// Leaves color unchanged.
108    None,
109    /// Reinhard tone mapping.
110    Reinhard,
111    /// ACES-inspired filmic curve.
112    #[default]
113    Aces,
114    /// Exponential exposure curve.
115    Exposure(f32),
116}
117
118/// Fast approximate anti-aliasing configuration.
119#[derive(Clone, Copy, Debug, PartialEq)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub struct FxaaConfig {
122    /// Minimum contrast that triggers smoothing.
123    pub contrast_threshold: f32,
124    /// Relative contrast threshold.
125    pub relative_threshold: f32,
126}
127
128impl FxaaConfig {
129    /// Returns a clamped configuration.
130    pub fn normalized(self) -> Self {
131        Self {
132            contrast_threshold: self.contrast_threshold.clamp(0.0, 1.0),
133            relative_threshold: self.relative_threshold.clamp(0.0, 1.0),
134        }
135    }
136}
137
138impl Default for FxaaConfig {
139    #[inline]
140    fn default() -> Self {
141        Self {
142            contrast_threshold: 0.0312,
143            relative_threshold: 0.125,
144        }
145    }
146}
147
148/// Temporal anti-aliasing blend configuration.
149#[derive(Clone, Copy, Debug, PartialEq)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub struct TaaConfig {
152    /// History feedback in `0..=1`.
153    pub feedback: f32,
154    /// Jitter amount in pixels.
155    pub jitter: f32,
156}
157
158impl TaaConfig {
159    /// Returns a clamped configuration.
160    pub fn normalized(self) -> Self {
161        Self {
162            feedback: self.feedback.clamp(0.0, 1.0),
163            jitter: self.jitter.clamp(0.0, 2.0),
164        }
165    }
166}
167
168impl Default for TaaConfig {
169    #[inline]
170    fn default() -> Self {
171        Self {
172            feedback: 0.9,
173            jitter: 0.5,
174        }
175    }
176}
177
178/// SMAA quality preset.
179#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub enum SmaaQuality {
182    /// Low quality.
183    Low,
184    /// Balanced quality.
185    #[default]
186    Medium,
187    /// High quality.
188    High,
189    /// Ultra quality.
190    Ultra,
191}
192
193/// SMAA configuration.
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196pub struct SmaaConfig {
197    /// Quality preset.
198    pub quality: SmaaQuality,
199}
200
201/// Depth-of-field post configuration.
202#[derive(Clone, Copy, Debug, PartialEq)]
203#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
204pub struct DofConfig {
205    /// Focus distance in scene units.
206    pub focus_distance: f32,
207    /// Aperture strength.
208    pub aperture: f32,
209    /// Maximum blur radius in pixels.
210    pub max_blur_radius: f32,
211}
212
213impl DofConfig {
214    /// Returns a clamped configuration.
215    pub fn normalized(self) -> Self {
216        Self {
217            focus_distance: self.focus_distance.max(0.001),
218            aperture: self.aperture.clamp(0.0, 32.0),
219            max_blur_radius: self.max_blur_radius.clamp(0.0, 64.0),
220        }
221    }
222}
223
224impl Default for DofConfig {
225    #[inline]
226    fn default() -> Self {
227        Self {
228            focus_distance: 10.0,
229            aperture: 1.4,
230            max_blur_radius: 8.0,
231        }
232    }
233}
234
235/// Fog blend post configuration.
236#[derive(Clone, Copy, Debug, PartialEq)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238pub struct FogPostConfig {
239    /// Fog color in linear RGB.
240    pub color: [f32; 3],
241    /// Fog density.
242    pub density: f32,
243}
244
245impl FogPostConfig {
246    /// Returns a clamped configuration.
247    pub fn normalized(self) -> Self {
248        Self {
249            color: [
250                self.color[0].clamp(0.0, 1.0),
251                self.color[1].clamp(0.0, 1.0),
252                self.color[2].clamp(0.0, 1.0),
253            ],
254            density: self.density.clamp(0.0, 1.0),
255        }
256    }
257}
258
259impl Default for FogPostConfig {
260    #[inline]
261    fn default() -> Self {
262        Self {
263            color: [0.5, 0.6, 0.7],
264            density: 0.05,
265        }
266    }
267}
268
269/// Edge outline post configuration.
270#[derive(Clone, Copy, Debug, PartialEq)]
271#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
272pub struct OutlineConfig {
273    /// Outline color in linear RGBA.
274    pub color: [f32; 4],
275    /// Luminance edge threshold.
276    pub threshold: f32,
277    /// Edge sampling distance in pixels.
278    pub thickness: f32,
279}
280
281impl OutlineConfig {
282    /// Returns a clamped configuration.
283    pub fn normalized(self) -> Self {
284        Self {
285            color: [
286                self.color[0].clamp(0.0, 1.0),
287                self.color[1].clamp(0.0, 1.0),
288                self.color[2].clamp(0.0, 1.0),
289                self.color[3].clamp(0.0, 1.0),
290            ],
291            threshold: self.threshold.clamp(0.0, 1.0),
292            thickness: self.thickness.clamp(0.0, 16.0),
293        }
294    }
295}
296
297impl Default for OutlineConfig {
298    #[inline]
299    fn default() -> Self {
300        Self {
301            color: [0.0, 0.0, 0.0, 1.0],
302            threshold: 0.1,
303            thickness: 1.0,
304        }
305    }
306}
307
308/// Camera motion blur post configuration.
309#[derive(Clone, Copy, Debug, PartialEq)]
310#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
311pub struct MotionBlurConfig {
312    /// Blur strength.
313    pub strength: f32,
314    /// Number of conceptual samples. The v0.7 shader maps this to a compact fixed pattern.
315    pub sample_count: u32,
316}
317
318impl MotionBlurConfig {
319    /// Returns a clamped configuration.
320    pub fn normalized(self) -> Self {
321        Self {
322            strength: self.strength.clamp(0.0, 1.0),
323            sample_count: self.sample_count.clamp(1, 32),
324        }
325    }
326}
327
328impl Default for MotionBlurConfig {
329    #[inline]
330    fn default() -> Self {
331        Self {
332            strength: 0.08,
333            sample_count: 8,
334        }
335    }
336}
337
338/// One post-processing effect in stack order.
339#[derive(Clone, Debug, PartialEq)]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341pub enum PostEffect {
342    /// Bloom pass.
343    Bloom(BloomConfig),
344    /// SSAO approximation pass.
345    Ssao(SsaoConfig),
346    /// Tonemapping pass.
347    Tonemap(ToneMapper),
348    /// FXAA pass.
349    Fxaa(FxaaConfig),
350    /// TAA blend pass.
351    Taa(TaaConfig),
352    /// SMAA pass.
353    Smaa(SmaaConfig),
354    /// Depth-of-field pass.
355    Dof(DofConfig),
356    /// Fog blend pass.
357    Fog(FogPostConfig),
358    /// Outline edge pass.
359    Outline(OutlineConfig),
360    /// Motion blur pass.
361    MotionBlur(MotionBlurConfig),
362}
363
364impl PostEffect {
365    /// Returns a stable numeric kind used by the WGSL shader.
366    #[inline]
367    pub const fn kind_id(&self) -> u32 {
368        match self {
369            Self::Bloom(_) => 1,
370            Self::Ssao(_) => 2,
371            Self::Tonemap(_) => 3,
372            Self::Fxaa(_) => 4,
373            Self::Taa(_) => 5,
374            Self::Smaa(_) => 6,
375            Self::Dof(_) => 7,
376            Self::Fog(_) => 8,
377            Self::Outline(_) => 9,
378            Self::MotionBlur(_) => 10,
379        }
380    }
381
382    fn params(&self) -> [f32; 8] {
383        match *self {
384            Self::Bloom(config) => {
385                let config = config.normalized();
386                [
387                    config.threshold,
388                    config.intensity,
389                    config.radius,
390                    0.0,
391                    self.kind_id() as f32,
392                    0.0,
393                    0.0,
394                    0.0,
395                ]
396            }
397            Self::Ssao(config) => {
398                let config = config.normalized();
399                [
400                    config.radius,
401                    config.intensity,
402                    config.bias,
403                    0.0,
404                    self.kind_id() as f32,
405                    0.0,
406                    0.0,
407                    0.0,
408                ]
409            }
410            Self::Tonemap(mapper) => {
411                let (mode, exposure) = match mapper {
412                    ToneMapper::None => (0.0, 1.0),
413                    ToneMapper::Reinhard => (1.0, 1.0),
414                    ToneMapper::Aces => (2.0, 1.0),
415                    ToneMapper::Exposure(exposure) => (3.0, exposure.max(0.0)),
416                };
417                [
418                    mode,
419                    exposure,
420                    0.0,
421                    0.0,
422                    self.kind_id() as f32,
423                    0.0,
424                    0.0,
425                    0.0,
426                ]
427            }
428            Self::Fxaa(config) => {
429                let config = config.normalized();
430                [
431                    config.contrast_threshold,
432                    config.relative_threshold,
433                    0.0,
434                    0.0,
435                    self.kind_id() as f32,
436                    0.0,
437                    0.0,
438                    0.0,
439                ]
440            }
441            Self::Taa(config) => {
442                let config = config.normalized();
443                [
444                    config.feedback,
445                    config.jitter,
446                    0.0,
447                    0.0,
448                    self.kind_id() as f32,
449                    0.0,
450                    0.0,
451                    0.0,
452                ]
453            }
454            Self::Smaa(config) => {
455                let quality = match config.quality {
456                    SmaaQuality::Low => 0.25,
457                    SmaaQuality::Medium => 0.5,
458                    SmaaQuality::High => 0.75,
459                    SmaaQuality::Ultra => 1.0,
460                };
461                [quality, 0.0, 0.0, 0.0, self.kind_id() as f32, 0.0, 0.0, 0.0]
462            }
463            Self::Dof(config) => {
464                let config = config.normalized();
465                [
466                    config.focus_distance,
467                    config.aperture,
468                    config.max_blur_radius,
469                    0.0,
470                    self.kind_id() as f32,
471                    0.0,
472                    0.0,
473                    0.0,
474                ]
475            }
476            Self::Fog(config) => {
477                let config = config.normalized();
478                [
479                    config.color[0],
480                    config.color[1],
481                    config.color[2],
482                    config.density,
483                    self.kind_id() as f32,
484                    0.0,
485                    0.0,
486                    0.0,
487                ]
488            }
489            Self::Outline(config) => {
490                let config = config.normalized();
491                [
492                    config.color[0],
493                    config.color[1],
494                    config.threshold,
495                    config.thickness,
496                    self.kind_id() as f32,
497                    config.color[2],
498                    config.color[3],
499                    0.0,
500                ]
501            }
502            Self::MotionBlur(config) => {
503                let config = config.normalized();
504                [
505                    config.strength,
506                    config.sample_count as f32,
507                    0.0,
508                    0.0,
509                    self.kind_id() as f32,
510                    0.0,
511                    0.0,
512                    0.0,
513                ]
514            }
515        }
516    }
517}
518
519/// Renderer-owned post-processing texture target.
520pub struct PostTarget {
521    texture: wgpu::Texture,
522    view: wgpu::TextureView,
523    width: u32,
524    height: u32,
525    format: wgpu::TextureFormat,
526}
527
528impl PostTarget {
529    /// Allocates a texture target suitable for post-processing.
530    pub fn new(
531        device: &wgpu::Device,
532        label: &str,
533        width: u32,
534        height: u32,
535        format: wgpu::TextureFormat,
536    ) -> Result<Self, ScenixError> {
537        if width == 0 || height == 0 {
538            return Err(ScenixError::Validation(ValidationError::OutOfRange));
539        }
540        let texture = device.create_texture(&wgpu::TextureDescriptor {
541            label: Some(label),
542            size: wgpu::Extent3d {
543                width,
544                height,
545                depth_or_array_layers: 1,
546            },
547            mip_level_count: 1,
548            sample_count: 1,
549            dimension: wgpu::TextureDimension::D2,
550            format,
551            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
552                | wgpu::TextureUsages::TEXTURE_BINDING
553                | wgpu::TextureUsages::COPY_SRC,
554            view_formats: &[],
555        });
556        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
557        Ok(Self {
558            texture,
559            view,
560            width,
561            height,
562            format,
563        })
564    }
565
566    /// Returns the texture view.
567    #[inline]
568    pub const fn view(&self) -> &wgpu::TextureView {
569        &self.view
570    }
571
572    /// Returns the backing texture.
573    #[inline]
574    pub const fn texture(&self) -> &wgpu::Texture {
575        &self.texture
576    }
577
578    /// Returns the target width.
579    #[inline]
580    pub const fn width(&self) -> u32 {
581        self.width
582    }
583
584    /// Returns the target height.
585    #[inline]
586    pub const fn height(&self) -> u32 {
587        self.height
588    }
589
590    /// Returns the texture format.
591    #[inline]
592    pub const fn format(&self) -> wgpu::TextureFormat {
593        self.format
594    }
595}
596
597/// Ordered post-processing stack with cached GPU resources.
598pub struct PostStack {
599    effects: Vec<PostEffect>,
600    bind_group_layout: Option<wgpu::BindGroupLayout>,
601    sampler: Option<wgpu::Sampler>,
602    uniform_buffer: Option<wgpu::Buffer>,
603    pipelines: Vec<(wgpu::TextureFormat, Arc<wgpu::RenderPipeline>)>,
604    scratch: [Option<PostTarget>; 2],
605}
606
607impl PostStack {
608    /// Creates an empty post stack.
609    #[inline]
610    pub fn new() -> Self {
611        Self {
612            effects: Vec::new(),
613            bind_group_layout: None,
614            sampler: None,
615            uniform_buffer: None,
616            pipelines: Vec::new(),
617            scratch: [None, None],
618        }
619    }
620
621    /// Returns the ordered effects.
622    #[inline]
623    pub fn effects(&self) -> &[PostEffect] {
624        &self.effects
625    }
626
627    /// Returns the number of effects in the stack.
628    #[inline]
629    pub fn len(&self) -> usize {
630        self.effects.len()
631    }
632
633    /// Returns whether the stack is empty.
634    #[inline]
635    pub fn is_empty(&self) -> bool {
636        self.effects.is_empty()
637    }
638
639    /// Adds a bloom pass.
640    pub fn with_bloom(mut self, config: BloomConfig) -> Self {
641        self.effects.push(PostEffect::Bloom(config.normalized()));
642        self
643    }
644
645    /// Adds an SSAO pass.
646    pub fn with_ssao(mut self, config: SsaoConfig) -> Self {
647        self.effects.push(PostEffect::Ssao(config.normalized()));
648        self
649    }
650
651    /// Adds a tone mapping pass.
652    pub fn with_tonemap(mut self, mapper: ToneMapper) -> Self {
653        self.effects.push(PostEffect::Tonemap(mapper));
654        self
655    }
656
657    /// Adds an FXAA pass.
658    pub fn with_fxaa(mut self, config: FxaaConfig) -> Self {
659        self.effects.push(PostEffect::Fxaa(config.normalized()));
660        self
661    }
662
663    /// Adds a TAA pass.
664    pub fn with_taa(mut self, config: TaaConfig) -> Self {
665        self.effects.push(PostEffect::Taa(config.normalized()));
666        self
667    }
668
669    /// Adds an SMAA pass.
670    pub fn with_smaa(mut self, config: SmaaConfig) -> Self {
671        self.effects.push(PostEffect::Smaa(config));
672        self
673    }
674
675    /// Adds a depth-of-field pass.
676    pub fn with_dof(mut self, config: DofConfig) -> Self {
677        self.effects.push(PostEffect::Dof(config.normalized()));
678        self
679    }
680
681    /// Adds a fog blend pass.
682    pub fn with_fog(mut self, config: FogPostConfig) -> Self {
683        self.effects.push(PostEffect::Fog(config.normalized()));
684        self
685    }
686
687    /// Adds an outline pass.
688    pub fn with_outline(mut self, config: OutlineConfig) -> Self {
689        self.effects.push(PostEffect::Outline(config.normalized()));
690        self
691    }
692
693    /// Adds a motion blur pass.
694    pub fn with_motion_blur(mut self, config: MotionBlurConfig) -> Self {
695        self.effects
696            .push(PostEffect::MotionBlur(config.normalized()));
697        self
698    }
699
700    /// Removes an effect by index.
701    pub fn remove(&mut self, index: usize) -> Option<PostEffect> {
702        if index < self.effects.len() {
703            Some(self.effects.remove(index))
704        } else {
705            None
706        }
707    }
708
709    /// Clears all effects while retaining GPU resources for reuse.
710    #[inline]
711    pub fn clear(&mut self) {
712        self.effects.clear();
713    }
714
715    /// Applies the stack from `source` to `output` and submits a GPU command buffer.
716    pub fn apply_to_view(
717        &mut self,
718        device: &wgpu::Device,
719        queue: &wgpu::Queue,
720        source: &wgpu::TextureView,
721        output: &wgpu::TextureView,
722        context: PostContext,
723    ) -> Result<PostStats, ScenixError> {
724        if self.effects.is_empty() {
725            return Ok(PostStats::default());
726        }
727        if context.resolution.x <= 0.0 || context.resolution.y <= 0.0 {
728            return Err(ScenixError::Validation(ValidationError::OutOfRange));
729        }
730
731        let width = context.resolution.x as u32;
732        let height = context.resolution.y as u32;
733        let resized_targets =
734            self.ensure_scratch_targets(device, width, height, context.color_format)?;
735        let pipeline = self.pipeline(device, context.color_format);
736        self.ensure_common_resources(device);
737
738        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
739            label: Some("scenix.post.encoder"),
740        });
741        let mut current_scratch: Option<usize> = None;
742
743        for index in 0..self.effects.len() {
744            let last = index + 1 == self.effects.len();
745            let destination_scratch = if last { None } else { Some(index % 2) };
746            let source_view = if let Some(scratch) = current_scratch {
747                self.scratch[scratch].as_ref().unwrap().view()
748            } else {
749                source
750            };
751            let destination_view = if let Some(scratch) = destination_scratch {
752                self.scratch[scratch].as_ref().unwrap().view()
753            } else {
754                output
755            };
756
757            let params = self.effects[index].params();
758            self.render_effect(EffectPass {
759                device,
760                queue,
761                pipeline: &pipeline,
762                encoder: &mut encoder,
763                source: source_view,
764                destination: destination_view,
765                params: &params,
766            });
767            current_scratch = destination_scratch;
768        }
769
770        queue.submit(Some(encoder.finish()));
771        Ok(PostStats {
772            passes: self.effects.len() as u32,
773            resized_targets,
774        })
775    }
776
777    fn ensure_scratch_targets(
778        &mut self,
779        device: &wgpu::Device,
780        width: u32,
781        height: u32,
782        format: wgpu::TextureFormat,
783    ) -> Result<u32, ScenixError> {
784        if self.effects.len() <= 1 {
785            return Ok(0);
786        }
787
788        let mut resized = 0;
789        for index in 0..2 {
790            let replace = self.scratch[index].as_ref().is_none_or(|target| {
791                target.width() != width || target.height() != height || target.format() != format
792            });
793            if replace {
794                self.scratch[index] = Some(PostTarget::new(
795                    device,
796                    if index == 0 {
797                        "scenix.post.scratch.0"
798                    } else {
799                        "scenix.post.scratch.1"
800                    },
801                    width,
802                    height,
803                    format,
804                )?);
805                resized += 1;
806            }
807        }
808        Ok(resized)
809    }
810
811    fn ensure_common_resources(&mut self, device: &wgpu::Device) {
812        if self.bind_group_layout.is_none() {
813            self.bind_group_layout = Some(device.create_bind_group_layout(
814                &wgpu::BindGroupLayoutDescriptor {
815                    label: Some("scenix.post.bind_group_layout"),
816                    entries: &[
817                        wgpu::BindGroupLayoutEntry {
818                            binding: 0,
819                            visibility: wgpu::ShaderStages::FRAGMENT,
820                            ty: wgpu::BindingType::Texture {
821                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
822                                view_dimension: wgpu::TextureViewDimension::D2,
823                                multisampled: false,
824                            },
825                            count: None,
826                        },
827                        wgpu::BindGroupLayoutEntry {
828                            binding: 1,
829                            visibility: wgpu::ShaderStages::FRAGMENT,
830                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
831                            count: None,
832                        },
833                        wgpu::BindGroupLayoutEntry {
834                            binding: 2,
835                            visibility: wgpu::ShaderStages::FRAGMENT,
836                            ty: wgpu::BindingType::Buffer {
837                                ty: wgpu::BufferBindingType::Uniform,
838                                has_dynamic_offset: false,
839                                min_binding_size: None,
840                            },
841                            count: None,
842                        },
843                    ],
844                },
845            ));
846        }
847        if self.sampler.is_none() {
848            self.sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
849                label: Some("scenix.post.sampler"),
850                mag_filter: wgpu::FilterMode::Linear,
851                min_filter: wgpu::FilterMode::Linear,
852                mipmap_filter: wgpu::MipmapFilterMode::Linear,
853                ..Default::default()
854            }));
855        }
856        if self.uniform_buffer.is_none() {
857            self.uniform_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
858                label: Some("scenix.post.uniforms"),
859                size: std::mem::size_of::<PostUniform>() as u64,
860                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
861                mapped_at_creation: false,
862            }));
863        }
864    }
865
866    fn pipeline(
867        &mut self,
868        device: &wgpu::Device,
869        format: wgpu::TextureFormat,
870    ) -> Arc<wgpu::RenderPipeline> {
871        if let Some((_, pipeline)) = self
872            .pipelines
873            .iter()
874            .find(|(pipeline_format, _)| *pipeline_format == format)
875        {
876            return Arc::clone(pipeline);
877        }
878
879        self.ensure_common_resources(device);
880        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
881            label: Some("scenix.post.shader"),
882            source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
883        });
884        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
885            label: Some("scenix.post.pipeline_layout"),
886            bind_group_layouts: &[Some(self.bind_group_layout.as_ref().unwrap())],
887            immediate_size: 0,
888        });
889        let pipeline = Arc::new(
890            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
891                label: Some("scenix.post.pipeline"),
892                layout: Some(&layout),
893                vertex: wgpu::VertexState {
894                    module: &shader,
895                    entry_point: Some("vs_main"),
896                    buffers: &[],
897                    compilation_options: wgpu::PipelineCompilationOptions::default(),
898                },
899                primitive: wgpu::PrimitiveState::default(),
900                depth_stencil: None,
901                multisample: wgpu::MultisampleState::default(),
902                fragment: Some(wgpu::FragmentState {
903                    module: &shader,
904                    entry_point: Some("fs_main"),
905                    targets: &[Some(wgpu::ColorTargetState {
906                        format,
907                        blend: Some(wgpu::BlendState::REPLACE),
908                        write_mask: wgpu::ColorWrites::ALL,
909                    })],
910                    compilation_options: wgpu::PipelineCompilationOptions::default(),
911                }),
912                multiview_mask: None,
913                cache: None,
914            }),
915        );
916        self.pipelines.push((format, Arc::clone(&pipeline)));
917        pipeline
918    }
919
920    fn render_effect(&self, pass: EffectPass<'_>) {
921        let uniform = PostUniform {
922            values: *pass.params,
923        };
924        pass.queue.write_buffer(
925            self.uniform_buffer.as_ref().unwrap(),
926            0,
927            bytemuck::bytes_of(&uniform),
928        );
929        let bind_group = pass.device.create_bind_group(&wgpu::BindGroupDescriptor {
930            label: Some("scenix.post.bind_group"),
931            layout: self.bind_group_layout.as_ref().unwrap(),
932            entries: &[
933                wgpu::BindGroupEntry {
934                    binding: 0,
935                    resource: wgpu::BindingResource::TextureView(pass.source),
936                },
937                wgpu::BindGroupEntry {
938                    binding: 1,
939                    resource: wgpu::BindingResource::Sampler(self.sampler.as_ref().unwrap()),
940                },
941                wgpu::BindGroupEntry {
942                    binding: 2,
943                    resource: self.uniform_buffer.as_ref().unwrap().as_entire_binding(),
944                },
945            ],
946        });
947        let mut render_pass = pass.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
948            label: Some("scenix.post.pass"),
949            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
950                view: pass.destination,
951                depth_slice: None,
952                resolve_target: None,
953                ops: wgpu::Operations {
954                    load: wgpu::LoadOp::Load,
955                    store: wgpu::StoreOp::Store,
956                },
957            })],
958            depth_stencil_attachment: None,
959            timestamp_writes: None,
960            occlusion_query_set: None,
961            multiview_mask: None,
962        });
963        render_pass.set_pipeline(pass.pipeline);
964        render_pass.set_bind_group(0, &bind_group, &[]);
965        render_pass.draw(0..3, 0..1);
966    }
967}
968
969impl Default for PostStack {
970    #[inline]
971    fn default() -> Self {
972        Self::new()
973    }
974}
975
976#[repr(C)]
977#[derive(Clone, Copy, Pod, Zeroable)]
978struct PostUniform {
979    values: [f32; 8],
980}
981
982struct EffectPass<'a> {
983    device: &'a wgpu::Device,
984    queue: &'a wgpu::Queue,
985    pipeline: &'a wgpu::RenderPipeline,
986    encoder: &'a mut wgpu::CommandEncoder,
987    source: &'a wgpu::TextureView,
988    destination: &'a wgpu::TextureView,
989    params: &'a [f32; 8],
990}