Skip to main content

loam_render/
composite.rs

1//! Browser WebGPU advertises only linear canvas formats, so the scene renders
2//! offscreen and this pass sRGB-encodes it into the swapchain. Native sRGB
3//! swapchains skip it.
4
5use wgpu::{
6    BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
7    BindGroupLayoutEntry, BindingResource, BindingType, BlendState, ColorTargetState, ColorWrites,
8    CommandEncoder, Device, FragmentState, LoadOp, MultisampleState, Operations,
9    PipelineLayoutDescriptor, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
10    RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor,
11    ShaderModuleDescriptor, ShaderSource, ShaderStages, StoreOp, TextureFormat, TextureSampleType,
12    TextureView, TextureViewDimension, VertexState,
13};
14
15pub struct CompositeNode {
16    pipeline: RenderPipeline,
17    sampler: Sampler,
18    bind_group_layout: BindGroupLayout,
19    bind_group: Option<BindGroup>,
20}
21
22impl CompositeNode {
23    pub fn new(device: &Device, target_format: TextureFormat) -> Self {
24        let shader = device.create_shader_module(ShaderModuleDescriptor {
25            label: Some("loam-render::composite::shader"),
26            source: ShaderSource::Wgsl(include_str!("composite.wgsl").into()),
27        });
28
29        let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
30            label: Some("loam-render::composite::bgl"),
31            entries: &[
32                BindGroupLayoutEntry {
33                    binding: 0,
34                    visibility: ShaderStages::FRAGMENT,
35                    ty: BindingType::Texture {
36                        sample_type: TextureSampleType::Float { filterable: true },
37                        view_dimension: TextureViewDimension::D2,
38                        multisampled: false,
39                    },
40                    count: None,
41                },
42                BindGroupLayoutEntry {
43                    binding: 1,
44                    visibility: ShaderStages::FRAGMENT,
45                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
46                    count: None,
47                },
48            ],
49        });
50
51        let sampler = device.create_sampler(&SamplerDescriptor {
52            label: Some("loam-render::composite::sampler"),
53            mag_filter: wgpu::FilterMode::Linear,
54            min_filter: wgpu::FilterMode::Linear,
55            mipmap_filter: wgpu::FilterMode::Nearest,
56            address_mode_u: wgpu::AddressMode::ClampToEdge,
57            address_mode_v: wgpu::AddressMode::ClampToEdge,
58            address_mode_w: wgpu::AddressMode::ClampToEdge,
59            ..Default::default()
60        });
61
62        let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
63            label: Some("loam-render::composite::layout"),
64            bind_group_layouts: &[&bind_group_layout],
65            push_constant_ranges: &[],
66        });
67
68        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
69            label: Some("loam-render::composite::pipeline"),
70            layout: Some(&layout),
71            vertex: VertexState {
72                module: &shader,
73                entry_point: Some("vs_fullscreen"),
74                buffers: &[],
75                compilation_options: Default::default(),
76            },
77            fragment: Some(FragmentState {
78                module: &shader,
79                entry_point: Some("fs_composite"),
80                targets: &[Some(ColorTargetState {
81                    format: target_format,
82                    blend: Some(BlendState::REPLACE),
83                    write_mask: ColorWrites::ALL,
84                })],
85                compilation_options: Default::default(),
86            }),
87            primitive: PrimitiveState {
88                topology: wgpu::PrimitiveTopology::TriangleList,
89                ..Default::default()
90            },
91            depth_stencil: None,
92            multisample: MultisampleState::default(),
93            multiview: None,
94            cache: None,
95        });
96
97        Self {
98            pipeline,
99            sampler,
100            bind_group_layout,
101            bind_group: None,
102        }
103    }
104
105    /// Call after a resize reallocates the scene texture.
106    pub fn set_scene_view(&mut self, device: &Device, scene_view: &TextureView) {
107        let bg = device.create_bind_group(&BindGroupDescriptor {
108            label: Some("loam-render::composite::bg"),
109            layout: &self.bind_group_layout,
110            entries: &[
111                BindGroupEntry {
112                    binding: 0,
113                    resource: BindingResource::TextureView(scene_view),
114                },
115                BindGroupEntry {
116                    binding: 1,
117                    resource: BindingResource::Sampler(&self.sampler),
118                },
119            ],
120        });
121        self.bind_group = Some(bg);
122    }
123
124    /// No-op before the first `set_scene_view`.
125    pub fn run(&self, encoder: &mut CommandEncoder, target_view: &TextureView) {
126        let Some(bind_group) = self.bind_group.as_ref() else {
127            return;
128        };
129        let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
130            label: Some("loam-render::composite::pass"),
131            color_attachments: &[Some(RenderPassColorAttachment {
132                view: target_view,
133                depth_slice: None,
134                resolve_target: None,
135                ops: Operations {
136                    load: LoadOp::Clear(wgpu::Color::BLACK),
137                    store: StoreOp::Store,
138                },
139            })],
140            depth_stencil_attachment: None,
141            timestamp_writes: None,
142            occlusion_query_set: None,
143        });
144        pass.set_pipeline(&self.pipeline);
145        pass.set_bind_group(0, bind_group, &[]);
146        pass.draw(0..3, 0..1);
147    }
148}