Skip to main content

mittens_engine/engine/graphics/
post_processing.rs

1use std::collections::{BTreeMap, HashMap};
2use std::mem::size_of;
3use std::sync::Arc;
4
5use crate::engine::graphics::vulkano_swapchain::VulkanoSwapchainState;
6use vulkano::buffer::BufferContents;
7use vulkano::command_buffer::{
8    AutoCommandBufferBuilder, PrimaryAutoCommandBuffer, RenderingAttachmentInfo, RenderingInfo,
9};
10use vulkano::descriptor_set::allocator::StandardDescriptorSetAllocator;
11use vulkano::descriptor_set::layout::{
12    DescriptorSetLayout, DescriptorSetLayoutBinding, DescriptorSetLayoutCreateInfo, DescriptorType,
13};
14use vulkano::descriptor_set::{DescriptorSet, WriteDescriptorSet};
15use vulkano::device::Device;
16use vulkano::format::{ClearValue, Format};
17use vulkano::image::sampler::{Filter, Sampler, SamplerAddressMode, SamplerCreateInfo};
18use vulkano::image::view::{ImageView, ImageViewCreateInfo};
19use vulkano::image::{
20    Image, ImageAspects, ImageCreateInfo, ImageSubresourceRange, ImageType, ImageUsage, SampleCount,
21};
22use vulkano::memory::allocator::{AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator};
23use vulkano::pipeline::graphics::color_blend::{ColorBlendAttachmentState, ColorBlendState};
24use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
25use vulkano::pipeline::graphics::multisample::MultisampleState;
26use vulkano::pipeline::graphics::rasterization::RasterizationState;
27use vulkano::pipeline::graphics::subpass::{PipelineRenderingCreateInfo, PipelineSubpassType};
28use vulkano::pipeline::graphics::vertex_input::VertexInputState;
29use vulkano::pipeline::graphics::viewport::{Scissor, Viewport, ViewportState};
30use vulkano::pipeline::layout::{PipelineLayout, PipelineLayoutCreateInfo, PushConstantRange};
31use vulkano::pipeline::{
32    DynamicState, GraphicsPipeline, Pipeline, PipelineBindPoint, PipelineShaderStageCreateInfo,
33};
34use vulkano::render_pass::{AttachmentLoadOp, AttachmentStoreOp};
35use vulkano::shader::ShaderStages;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum BloomSource {
39    #[default]
40    Emissive,
41}
42
43#[derive(Debug, Clone)]
44pub struct EmissivePassConfig {
45    pub output_texture: Option<String>,
46}
47
48impl Default for EmissivePassConfig {
49    fn default() -> Self {
50        Self {
51            output_texture: None,
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub struct BlurPassConfig {
58    pub enabled: bool,
59    pub radius_ndc: f32,
60    pub half_res: bool,
61}
62
63impl Default for BlurPassConfig {
64    fn default() -> Self {
65        Self {
66            enabled: true,
67            radius_ndc: 0.05,
68            half_res: true,
69        }
70    }
71}
72
73#[derive(Debug, Clone)]
74pub struct BloomConfig {
75    pub intensity: f32,
76    pub radius_ndc: f32,
77    pub emissive_scale: f32,
78    pub half_res: bool,
79    pub source: BloomSource,
80    pub output_texture: Option<String>,
81}
82
83impl Default for BloomConfig {
84    fn default() -> Self {
85        Self {
86            intensity: 1.0,
87            radius_ndc: 0.05,
88            emissive_scale: 1.0,
89            half_res: true,
90            source: BloomSource::Emissive,
91            output_texture: None,
92        }
93    }
94}
95
96#[derive(Debug, Clone)]
97pub struct BokehConfig {
98    pub focus_distance: f32,
99    pub aperture: f32,
100    pub max_blur_radius: f32,
101}
102
103impl Default for BokehConfig {
104    fn default() -> Self {
105        Self {
106            focus_distance: 2.0,
107            aperture: 0.0,
108            max_blur_radius: 8.0,
109        }
110    }
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct PostProcessingConfig {
115    pub enabled: bool,
116    pub emissive_pass: Option<EmissivePassConfig>,
117    pub blur_pass: Option<BlurPassConfig>,
118    pub bloom: Option<BloomConfig>,
119    pub bokeh: Option<BokehConfig>,
120}
121
122impl PostProcessingConfig {
123    pub fn is_active(&self) -> bool {
124        self.enabled && (self.bloom.is_some() || self.bokeh.is_some())
125    }
126
127    pub fn needs_depth(&self) -> bool {
128        self.enabled && (self.bloom.is_some() || self.bokeh.is_some())
129    }
130
131    pub fn bloom_radius_pixels(&self, viewport_width: u32) -> Option<u32> {
132        let bloom = self.bloom.as_ref()?;
133        Some(Self::ndc_radius_to_pixels(bloom.radius_ndc, viewport_width))
134    }
135
136    /// Effective blur radius: uses `BlurPassConfig` when present, falls back to bloom.
137    pub fn effective_blur_radius_pixels(&self, viewport_width: u32) -> Option<u32> {
138        if let Some(blur) = &self.blur_pass {
139            if blur.enabled {
140                return Some(Self::ndc_radius_to_pixels(blur.radius_ndc, viewport_width));
141            }
142        }
143        self.bloom_radius_pixels(viewport_width)
144    }
145
146    /// Effective half-res flag: uses `BlurPassConfig` when present, falls back to bloom.
147    pub fn effective_blur_half_res(&self) -> bool {
148        if let Some(blur) = &self.blur_pass {
149            if blur.enabled {
150                return blur.half_res;
151            }
152        }
153        self.bloom.as_ref().map(|b| b.half_res).unwrap_or(false)
154    }
155
156    pub fn ndc_radius_to_pixels(radius_ndc: f32, viewport_width: u32) -> u32 {
157        ((radius_ndc.max(0.0) * viewport_width as f32) / 2.0)
158            .round()
159            .max(1.0) as u32
160    }
161}
162
163#[derive(Debug, Clone)]
164pub struct PostProcessFrameTargets {
165    pub main_color: Arc<ImageView>,
166    pub main_msaa_color: Option<Arc<ImageView>>,
167    pub depth: Arc<ImageView>,
168    pub bloom_source_msaa: Option<Arc<ImageView>>,
169    pub bloom_source: Option<Arc<ImageView>>,
170    pub bloom_a: Option<Arc<ImageView>>,
171    pub bloom_b: Option<Arc<ImageView>>,
172    pub bloom_extent: [u32; 2],
173}
174
175#[derive(Debug, Clone)]
176struct PostProcessTargetSet {
177    extent: [u32; 2],
178    color_format: Format,
179    msaa_samples: SampleCount,
180    frames: Vec<PostProcessFrameTargets>,
181}
182
183#[derive(BufferContents, Clone, Copy)]
184#[repr(C)]
185struct PostProcessPushConstants {
186    direction: [f32; 2],
187    bloom_intensity: f32,
188    radius_pixels: f32,
189}
190
191mod fullscreen_vs {
192    vulkano_shaders::shader! {
193        ty: "vertex",
194        path: "assets/shaders/post-process-fullscreen.vert",
195    }
196}
197
198mod blit_fs {
199    vulkano_shaders::shader! {
200        ty: "fragment",
201        path: "assets/shaders/post-process-copy.frag",
202    }
203}
204
205mod blur_fs {
206    vulkano_shaders::shader! {
207        ty: "fragment",
208        path: "assets/shaders/post-process-bloom-blur.frag",
209    }
210}
211
212mod composite_fs {
213    vulkano_shaders::shader! {
214        ty: "fragment",
215        path: "assets/shaders/post-process-bloom-composite.frag",
216    }
217}
218
219pub struct PostProcessingRenderer {
220    device: Arc<Device>,
221    memory_allocator: Arc<StandardMemoryAllocator>,
222    descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
223    sampler_linear: Arc<Sampler>,
224    sampled_layout: Arc<DescriptorSetLayout>,
225    blit_pipelines: HashMap<Format, Arc<GraphicsPipeline>>,
226    blur_pipelines: HashMap<Format, Arc<GraphicsPipeline>>,
227    composite_pipelines: HashMap<Format, Arc<GraphicsPipeline>>,
228    window_targets: Option<PostProcessTargetSet>,
229    xr_targets: Option<PostProcessTargetSet>,
230}
231
232impl PostProcessingRenderer {
233    pub fn new(
234        device: Arc<Device>,
235        memory_allocator: Arc<StandardMemoryAllocator>,
236        descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
237    ) -> Result<Self, Box<dyn std::error::Error>> {
238        let mut bindings = BTreeMap::new();
239
240        let mut src0 =
241            DescriptorSetLayoutBinding::descriptor_type(DescriptorType::CombinedImageSampler);
242        src0.descriptor_count = 1;
243        src0.stages = ShaderStages::FRAGMENT;
244        bindings.insert(0, src0);
245
246        let mut src1 =
247            DescriptorSetLayoutBinding::descriptor_type(DescriptorType::CombinedImageSampler);
248        src1.descriptor_count = 1;
249        src1.stages = ShaderStages::FRAGMENT;
250        bindings.insert(1, src1);
251
252        let sampled_layout = DescriptorSetLayout::new(
253            device.clone(),
254            DescriptorSetLayoutCreateInfo {
255                bindings,
256                ..Default::default()
257            },
258        )?;
259
260        let sampler_linear = Sampler::new(
261            device.clone(),
262            SamplerCreateInfo {
263                mag_filter: Filter::Linear,
264                min_filter: Filter::Linear,
265                address_mode: [SamplerAddressMode::ClampToEdge; 3],
266                ..Default::default()
267            },
268        )?;
269
270        Ok(Self {
271            device,
272            memory_allocator,
273            descriptor_set_allocator,
274            sampler_linear,
275            sampled_layout,
276            blit_pipelines: HashMap::new(),
277            blur_pipelines: HashMap::new(),
278            composite_pipelines: HashMap::new(),
279            window_targets: None,
280            xr_targets: None,
281        })
282    }
283
284    pub fn ensure_window_targets(
285        &mut self,
286        frame_count: usize,
287        extent: [u32; 2],
288        color_format: Format,
289        msaa_samples: SampleCount,
290        config: &PostProcessingConfig,
291    ) -> Result<(), Box<dyn std::error::Error>> {
292        if !config.is_active() {
293            self.window_targets = None;
294            return Ok(());
295        }
296
297        let needs_recreate = self.window_targets.as_ref().is_none_or(|targets| {
298            targets.extent != extent
299                || targets.color_format != color_format
300                || targets.msaa_samples != msaa_samples
301                || targets.frames.len() != frame_count
302        });
303
304        if needs_recreate {
305            self.window_targets = Some(self.build_target_set(
306                frame_count,
307                extent,
308                color_format,
309                msaa_samples,
310                config,
311            )?);
312        }
313
314        Ok(())
315    }
316
317    pub fn ensure_xr_targets(
318        &mut self,
319        view_count: usize,
320        extent: [u32; 2],
321        color_format: Format,
322        msaa_samples: SampleCount,
323        config: &PostProcessingConfig,
324    ) -> Result<(), Box<dyn std::error::Error>> {
325        if !config.is_active() {
326            self.xr_targets = None;
327            return Ok(());
328        }
329
330        let needs_recreate = self.xr_targets.as_ref().is_none_or(|targets| {
331            targets.extent != extent
332                || targets.color_format != color_format
333                || targets.msaa_samples != msaa_samples
334                || targets.frames.len() != view_count
335        });
336
337        if needs_recreate {
338            self.xr_targets = Some(self.build_target_set(
339                view_count,
340                extent,
341                color_format,
342                msaa_samples,
343                config,
344            )?);
345        }
346
347        Ok(())
348    }
349
350    pub fn window_frame_targets(&self, frame: usize) -> Option<&PostProcessFrameTargets> {
351        self.window_targets.as_ref()?.frames.get(frame)
352    }
353
354    pub fn xr_frame_targets(&self, eye: usize) -> Option<&PostProcessFrameTargets> {
355        self.xr_targets.as_ref()?.frames.get(eye)
356    }
357
358    pub fn record_blur_pass(
359        &mut self,
360        cbb: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
361        color_format: Format,
362        source: Arc<ImageView>,
363        destination: Arc<ImageView>,
364        extent: [u32; 2],
365        direction: [f32; 2],
366        radius_pixels: u32,
367    ) -> Result<(), Box<dyn std::error::Error>> {
368        let pipeline = self.blur_pipeline(color_format)?;
369        let set = self.sampled_set(source.clone(), source)?;
370        self.record_fullscreen_pass(
371            cbb,
372            destination,
373            extent,
374            pipeline,
375            set,
376            PostProcessPushConstants {
377                direction,
378                bloom_intensity: 0.0,
379                radius_pixels: radius_pixels as f32,
380            },
381        )
382    }
383
384    pub fn record_final_pass(
385        &mut self,
386        cbb: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
387        color_format: Format,
388        output: Arc<ImageView>,
389        extent: [u32; 2],
390        main_color: Arc<ImageView>,
391        bloom_color: Option<Arc<ImageView>>,
392        config: &PostProcessingConfig,
393    ) -> Result<(), Box<dyn std::error::Error>> {
394        let (pipeline, set) = if let Some(bloom_view) = bloom_color {
395            (
396                self.composite_pipeline(color_format)?,
397                self.sampled_set(main_color, bloom_view)?,
398            )
399        } else {
400            (
401                self.blit_pipeline(color_format)?,
402                self.sampled_set(main_color.clone(), main_color)?,
403            )
404        };
405
406        self.record_fullscreen_pass(
407            cbb,
408            output,
409            extent,
410            pipeline,
411            set,
412            PostProcessPushConstants {
413                direction: [0.0, 0.0],
414                bloom_intensity: config
415                    .bloom
416                    .as_ref()
417                    .map(|b| b.intensity.max(0.0))
418                    .unwrap_or(0.0),
419                radius_pixels: 0.0,
420            },
421        )
422    }
423
424    fn record_fullscreen_pass(
425        &self,
426        cbb: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
427        output: Arc<ImageView>,
428        extent: [u32; 2],
429        pipeline: Arc<GraphicsPipeline>,
430        set: Arc<DescriptorSet>,
431        push_constants: PostProcessPushConstants,
432    ) -> Result<(), Box<dyn std::error::Error>> {
433        let viewport = Viewport {
434            offset: [0.0, 0.0],
435            extent: [extent[0] as f32, extent[1] as f32],
436            depth_range: 0.0..=1.0,
437            ..Default::default()
438        };
439
440        cbb.begin_rendering(RenderingInfo {
441            render_area_offset: [0, 0],
442            render_area_extent: extent,
443            layer_count: 1,
444            color_attachments: vec![Some(RenderingAttachmentInfo {
445                load_op: AttachmentLoadOp::Clear,
446                store_op: AttachmentStoreOp::Store,
447                clear_value: Some(ClearValue::from([0.0, 0.0, 0.0, 1.0])),
448                ..RenderingAttachmentInfo::image_view(output)
449            })],
450            ..Default::default()
451        })?;
452
453        cbb.set_viewport(0, vec![viewport].into())?;
454        cbb.set_scissor(
455            0,
456            vec![Scissor {
457                offset: [0, 0],
458                extent,
459                ..Default::default()
460            }]
461            .into(),
462        )?;
463        cbb.bind_pipeline_graphics(pipeline.clone())?;
464        cbb.bind_descriptor_sets(
465            PipelineBindPoint::Graphics,
466            pipeline.layout().clone(),
467            0,
468            set,
469        )?;
470        cbb.push_constants(pipeline.layout().clone(), 0, push_constants)?;
471        unsafe {
472            cbb.draw(3, 1, 0, 0)?;
473        }
474        cbb.end_rendering()?;
475        Ok(())
476    }
477
478    fn build_target_set(
479        &self,
480        count: usize,
481        extent: [u32; 2],
482        color_format: Format,
483        msaa_samples: SampleCount,
484        config: &PostProcessingConfig,
485    ) -> Result<PostProcessTargetSet, Box<dyn std::error::Error>> {
486        let bloom_extent = if config.effective_blur_half_res() {
487            [extent[0].max(2) / 2, extent[1].max(2) / 2]
488        } else {
489            extent
490        };
491
492        let mut frames = Vec::with_capacity(count);
493        for _ in 0..count {
494            let main_color = Self::create_color_view(
495                self.memory_allocator.clone(),
496                color_format,
497                extent,
498                SampleCount::Sample1,
499                ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC,
500            )?;
501            let main_msaa_color = if msaa_samples != SampleCount::Sample1 {
502                Some(Self::create_color_view(
503                    self.memory_allocator.clone(),
504                    color_format,
505                    extent,
506                    msaa_samples,
507                    ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSIENT_ATTACHMENT,
508                )?)
509            } else {
510                None
511            };
512            let depth =
513                Self::create_depth_view(self.memory_allocator.clone(), extent, msaa_samples)?;
514
515            let (bloom_source_msaa, bloom_source, bloom_a, bloom_b) = if config.bloom.is_some() {
516                (
517                    if msaa_samples != SampleCount::Sample1 {
518                        Some(Self::create_color_view(
519                            self.memory_allocator.clone(),
520                            color_format,
521                            extent,
522                            msaa_samples,
523                            ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSIENT_ATTACHMENT,
524                        )?)
525                    } else {
526                        None
527                    },
528                    Some(Self::create_color_view(
529                        self.memory_allocator.clone(),
530                        color_format,
531                        extent,
532                        SampleCount::Sample1,
533                        ImageUsage::COLOR_ATTACHMENT
534                            | ImageUsage::SAMPLED
535                            | ImageUsage::TRANSFER_SRC,
536                    )?),
537                    Some(Self::create_color_view(
538                        self.memory_allocator.clone(),
539                        color_format,
540                        bloom_extent,
541                        SampleCount::Sample1,
542                        ImageUsage::COLOR_ATTACHMENT
543                            | ImageUsage::SAMPLED
544                            | ImageUsage::TRANSFER_SRC,
545                    )?),
546                    Some(Self::create_color_view(
547                        self.memory_allocator.clone(),
548                        color_format,
549                        bloom_extent,
550                        SampleCount::Sample1,
551                        ImageUsage::COLOR_ATTACHMENT
552                            | ImageUsage::SAMPLED
553                            | ImageUsage::TRANSFER_SRC,
554                    )?),
555                )
556            } else {
557                (None, None, None, None)
558            };
559
560            frames.push(PostProcessFrameTargets {
561                main_color,
562                main_msaa_color,
563                depth,
564                bloom_source_msaa,
565                bloom_source,
566                bloom_a,
567                bloom_b,
568                bloom_extent,
569            });
570        }
571
572        Ok(PostProcessTargetSet {
573            extent,
574            color_format,
575            msaa_samples,
576            frames,
577        })
578    }
579
580    fn create_color_view(
581        memory_allocator: Arc<StandardMemoryAllocator>,
582        format: Format,
583        extent: [u32; 2],
584        samples: SampleCount,
585        usage: ImageUsage,
586    ) -> Result<Arc<ImageView>, Box<dyn std::error::Error>> {
587        let image = Image::new(
588            memory_allocator,
589            ImageCreateInfo {
590                image_type: ImageType::Dim2d,
591                format,
592                extent: [extent[0], extent[1], 1],
593                samples,
594                usage,
595                ..Default::default()
596            },
597            AllocationCreateInfo {
598                memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
599                ..Default::default()
600            },
601        )?;
602        Ok(ImageView::new_default(image)?)
603    }
604
605    fn create_depth_view(
606        memory_allocator: Arc<StandardMemoryAllocator>,
607        extent: [u32; 2],
608        samples: SampleCount,
609    ) -> Result<Arc<ImageView>, Box<dyn std::error::Error>> {
610        let image = Image::new(
611            memory_allocator,
612            ImageCreateInfo {
613                image_type: ImageType::Dim2d,
614                format: VulkanoSwapchainState::DEPTH_FORMAT,
615                extent: [extent[0], extent[1], 1],
616                samples,
617                usage: ImageUsage::DEPTH_STENCIL_ATTACHMENT,
618                ..Default::default()
619            },
620            AllocationCreateInfo {
621                memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
622                ..Default::default()
623            },
624        )?;
625        Ok(ImageView::new(
626            image.clone(),
627            ImageViewCreateInfo {
628                subresource_range: ImageSubresourceRange {
629                    aspects: ImageAspects::DEPTH | ImageAspects::STENCIL,
630                    ..image.subresource_range()
631                },
632                ..ImageViewCreateInfo::from_image(&image)
633            },
634        )?)
635    }
636
637    fn sampled_set(
638        &self,
639        source0: Arc<ImageView>,
640        source1: Arc<ImageView>,
641    ) -> Result<Arc<DescriptorSet>, Box<dyn std::error::Error>> {
642        Ok(DescriptorSet::new(
643            self.descriptor_set_allocator.clone(),
644            self.sampled_layout.clone(),
645            [
646                WriteDescriptorSet::image_view_sampler(0, source0, self.sampler_linear.clone()),
647                WriteDescriptorSet::image_view_sampler(1, source1, self.sampler_linear.clone()),
648            ],
649            [],
650        )?)
651    }
652
653    fn blit_pipeline(
654        &mut self,
655        color_format: Format,
656    ) -> Result<Arc<GraphicsPipeline>, Box<dyn std::error::Error>> {
657        if let Some(existing) = self.blit_pipelines.get(&color_format) {
658            return Ok(existing.clone());
659        }
660
661        let pipeline = self.build_pipeline(
662            color_format,
663            blit_fs::load(self.device.clone())?
664                .entry_point("main")
665                .ok_or("missing post-process-copy.frag entry point")?,
666        )?;
667        self.blit_pipelines.insert(color_format, pipeline.clone());
668        Ok(pipeline)
669    }
670
671    fn blur_pipeline(
672        &mut self,
673        color_format: Format,
674    ) -> Result<Arc<GraphicsPipeline>, Box<dyn std::error::Error>> {
675        if let Some(existing) = self.blur_pipelines.get(&color_format) {
676            return Ok(existing.clone());
677        }
678
679        let pipeline = self.build_pipeline(
680            color_format,
681            blur_fs::load(self.device.clone())?
682                .entry_point("main")
683                .ok_or("missing post-process-bloom-blur.frag entry point")?,
684        )?;
685        self.blur_pipelines.insert(color_format, pipeline.clone());
686        Ok(pipeline)
687    }
688
689    fn composite_pipeline(
690        &mut self,
691        color_format: Format,
692    ) -> Result<Arc<GraphicsPipeline>, Box<dyn std::error::Error>> {
693        if let Some(existing) = self.composite_pipelines.get(&color_format) {
694            return Ok(existing.clone());
695        }
696
697        let pipeline = self.build_pipeline(
698            color_format,
699            composite_fs::load(self.device.clone())?
700                .entry_point("main")
701                .ok_or("missing post-process-bloom-composite.frag entry point")?,
702        )?;
703        self.composite_pipelines
704            .insert(color_format, pipeline.clone());
705        Ok(pipeline)
706    }
707
708    fn build_pipeline(
709        &self,
710        color_format: Format,
711        fragment_entry: vulkano::shader::EntryPoint,
712    ) -> Result<Arc<GraphicsPipeline>, Box<dyn std::error::Error>> {
713        let vs = fullscreen_vs::load(self.device.clone())?;
714        let layout = PipelineLayout::new(
715            self.device.clone(),
716            PipelineLayoutCreateInfo {
717                set_layouts: vec![self.sampled_layout.clone()],
718                push_constant_ranges: vec![PushConstantRange {
719                    stages: ShaderStages::FRAGMENT,
720                    offset: 0,
721                    size: size_of::<PostProcessPushConstants>() as u32,
722                }],
723                ..Default::default()
724            },
725        )?;
726
727        let mut ci = vulkano::pipeline::graphics::GraphicsPipelineCreateInfo::layout(layout);
728        ci.stages = vec![
729            PipelineShaderStageCreateInfo::new(
730                vs.entry_point("main")
731                    .ok_or("missing post-process-fullscreen.vert entry point")?,
732            ),
733            PipelineShaderStageCreateInfo::new(fragment_entry),
734        ]
735        .into();
736        ci.vertex_input_state = Some(VertexInputState::default());
737        ci.input_assembly_state = Some(InputAssemblyState::default());
738        ci.viewport_state = Some(ViewportState::default());
739        ci.rasterization_state = Some(RasterizationState::default());
740        ci.multisample_state = Some(MultisampleState::default());
741        ci.color_blend_state = Some(ColorBlendState::with_attachment_states(
742            1,
743            ColorBlendAttachmentState::default(),
744        ));
745        ci.dynamic_state = [DynamicState::Viewport, DynamicState::Scissor]
746            .into_iter()
747            .collect();
748
749        let mut rendering = PipelineRenderingCreateInfo::default();
750        rendering.color_attachment_formats = vec![Some(color_format)];
751        ci.subpass = Some(PipelineSubpassType::BeginRendering(rendering));
752
753        Ok(GraphicsPipeline::new(self.device.clone(), None, ci)?)
754    }
755}