Skip to main content

wisp/render/
mask_combine.rs

1//! Boolean operations on alpha-mask textures
2//! (M-VEC.11 / AUT-63).
3//!
4//! Pipeline samples two mask `RenderTexture`s and writes their
5//! union / intersect / subtract into a third mask RT. Consumers
6//! still sample alpha (or RGB; both store the same value).
7
8use bytemuck::{Pod, Zeroable};
9use wgpu::util::DeviceExt;
10
11use crate::application::Application;
12use crate::texture::render_texture::RenderTexture;
13
14/// Boolean operation between two masks.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum MaskCombineOp {
17    /// `max(a, b)` — pixel covered by either mask.
18    Union,
19    /// `a * b` — pixel covered by both masks.
20    Intersect,
21    /// `a * (1 - b)` — pixel covered by `a` but not `b`.
22    Subtract,
23}
24
25impl MaskCombineOp {
26    fn shader_code(self) -> u32 {
27        match self {
28            Self::Union => 0,
29            Self::Intersect => 1,
30            Self::Subtract => 2,
31        }
32    }
33}
34
35#[repr(C, align(16))]
36#[derive(Clone, Copy, Pod, Zeroable)]
37struct CombineUniforms {
38    // WGSL `Uniforms { op: u32, _pad: vec3<u32> }` is 32 bytes
39    // (vec3<u32> is 16-byte aligned, expanding the struct). Match
40    // that layout exactly here: op + 3 ignored u32s + 4 trailing
41    // padding u32s = 32 bytes.
42    op: u32,
43    _pad: [u32; 7],
44}
45
46pub(crate) struct MaskCombinePipeline {
47    pipeline: wgpu::RenderPipeline,
48    bind_group_layout: wgpu::BindGroupLayout,
49    sampler: wgpu::Sampler,
50}
51
52impl MaskCombinePipeline {
53    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
54        let device = app.device();
55
56        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
57            label: Some("wisp::mask_combine bg layout"),
58            entries: &[
59                wgpu::BindGroupLayoutEntry {
60                    binding: 0,
61                    visibility: wgpu::ShaderStages::FRAGMENT,
62                    ty: wgpu::BindingType::Texture {
63                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
64                        view_dimension: wgpu::TextureViewDimension::D2,
65                        multisampled: false,
66                    },
67                    count: None,
68                },
69                wgpu::BindGroupLayoutEntry {
70                    binding: 1,
71                    visibility: wgpu::ShaderStages::FRAGMENT,
72                    ty: wgpu::BindingType::Texture {
73                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
74                        view_dimension: wgpu::TextureViewDimension::D2,
75                        multisampled: false,
76                    },
77                    count: None,
78                },
79                wgpu::BindGroupLayoutEntry {
80                    binding: 2,
81                    visibility: wgpu::ShaderStages::FRAGMENT,
82                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
83                    count: None,
84                },
85                wgpu::BindGroupLayoutEntry {
86                    binding: 3,
87                    visibility: wgpu::ShaderStages::FRAGMENT,
88                    ty: wgpu::BindingType::Buffer {
89                        ty: wgpu::BufferBindingType::Uniform,
90                        has_dynamic_offset: false,
91                        min_binding_size: None,
92                    },
93                    count: None,
94                },
95            ],
96        });
97
98        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
99            label: Some("wisp::mask_combine pipeline layout"),
100            bind_group_layouts: &[&bind_group_layout],
101            push_constant_ranges: &[],
102        });
103
104        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
105            label: Some("wisp::mask_combine shader"),
106            source: wgpu::ShaderSource::Wgsl(
107                include_str!("../../shaders/mask_combine.wgsl").into(),
108            ),
109        });
110
111        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
112            label: Some("wisp::mask_combine pipeline"),
113            layout: Some(&pipeline_layout),
114            vertex: wgpu::VertexState {
115                module: &shader,
116                entry_point: Some("main_vs"),
117                buffers: &[],
118                compilation_options: wgpu::PipelineCompilationOptions::default(),
119            },
120            fragment: Some(wgpu::FragmentState {
121                module: &shader,
122                entry_point: Some("main_fs"),
123                targets: &[Some(wgpu::ColorTargetState {
124                    format: output_format,
125                    blend: Some(wgpu::BlendState::REPLACE),
126                    write_mask: wgpu::ColorWrites::ALL,
127                })],
128                compilation_options: wgpu::PipelineCompilationOptions::default(),
129            }),
130            primitive: wgpu::PrimitiveState::default(),
131            depth_stencil: None,
132            multisample: wgpu::MultisampleState::default(),
133            multiview: None,
134            cache: None,
135        });
136
137        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
138            label: Some("wisp::mask_combine sampler"),
139            address_mode_u: wgpu::AddressMode::ClampToEdge,
140            address_mode_v: wgpu::AddressMode::ClampToEdge,
141            address_mode_w: wgpu::AddressMode::ClampToEdge,
142            mag_filter: wgpu::FilterMode::Linear,
143            min_filter: wgpu::FilterMode::Linear,
144            mipmap_filter: wgpu::FilterMode::Nearest,
145            ..Default::default()
146        });
147
148        Self {
149            pipeline,
150            bind_group_layout,
151            sampler,
152        }
153    }
154
155    pub(crate) fn apply(
156        &self,
157        app: &Application,
158        a: &RenderTexture,
159        b: &RenderTexture,
160        op: MaskCombineOp,
161        output: &RenderTexture,
162    ) {
163        let uniforms = CombineUniforms {
164            op: op.shader_code(),
165            _pad: [0; 7],
166        };
167        let buffer = app
168            .device()
169            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
170                label: Some("wisp::mask_combine uniforms"),
171                contents: bytemuck::bytes_of(&uniforms),
172                usage: wgpu::BufferUsages::UNIFORM,
173            });
174
175        let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
176            label: Some("wisp::mask_combine bg"),
177            layout: &self.bind_group_layout,
178            entries: &[
179                wgpu::BindGroupEntry {
180                    binding: 0,
181                    resource: wgpu::BindingResource::TextureView(a.view()),
182                },
183                wgpu::BindGroupEntry {
184                    binding: 1,
185                    resource: wgpu::BindingResource::TextureView(b.view()),
186                },
187                wgpu::BindGroupEntry {
188                    binding: 2,
189                    resource: wgpu::BindingResource::Sampler(&self.sampler),
190                },
191                wgpu::BindGroupEntry {
192                    binding: 3,
193                    resource: buffer.as_entire_binding(),
194                },
195            ],
196        });
197
198        let mut encoder = app
199            .device()
200            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
201                label: Some("wisp::mask_combine encoder"),
202            });
203        {
204            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
205                label: Some("wisp::mask_combine pass"),
206                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
207                    view: output.view(),
208                    resolve_target: None,
209                    ops: wgpu::Operations {
210                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
211                        store: wgpu::StoreOp::Store,
212                    },
213                })],
214                depth_stencil_attachment: None,
215                timestamp_writes: None,
216                occlusion_query_set: None,
217            });
218            pass.set_pipeline(&self.pipeline);
219            pass.set_bind_group(0, &bg, &[]);
220            pass.draw(0..3, 0..1);
221        }
222        app.queue().submit(std::iter::once(encoder.finish()));
223    }
224}