Skip to main content

wisp/render/
mask_texture.rs

1//! Generate an alpha-mask `RenderTexture` from a [`MaskShape`]
2//! (M-DYN.1 / AUT-43).
3//!
4//! The output texture stores coverage as `(m, m, m, m)`: same value
5//! in RGB and alpha so consumers can either alpha-multiply (sample
6//! `.a`) or display as a grayscale silhouette. The mask is decoupled
7//! from foreground sampling — composition happens in a downstream
8//! pipeline.
9//!
10//! Architectural rationale (`AUT-43`): this primitive owns *only*
11//! coverage. Privacy blur, redaction, and spotlight will compose with
12//! these textures via separate paths. The cache (`AUT-44`) layers on
13//! top to avoid regenerating identical masks every frame.
14
15use bytemuck::{Pod, Zeroable};
16use wgpu::util::DeviceExt;
17
18use crate::application::Application;
19use crate::scene::clip::MaskShape;
20use crate::texture::render_texture::RenderTexture;
21
22#[repr(C)]
23#[derive(Clone, Copy, Pod, Zeroable)]
24struct MaskTextureUniforms {
25    center: [f32; 2],
26    half_extents: [f32; 2],
27    radius: f32,
28    aa: f32,
29    invert: f32,
30    shape_kind: f32,
31}
32
33pub(crate) struct MaskTexturePipeline {
34    pipeline: wgpu::RenderPipeline,
35    bind_group_layout: wgpu::BindGroupLayout,
36}
37
38impl MaskTexturePipeline {
39    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
40        let device = app.device();
41
42        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
43            label: Some("wisp::mask_texture bg layout"),
44            entries: &[wgpu::BindGroupLayoutEntry {
45                binding: 0,
46                visibility: wgpu::ShaderStages::FRAGMENT,
47                ty: wgpu::BindingType::Buffer {
48                    ty: wgpu::BufferBindingType::Uniform,
49                    has_dynamic_offset: false,
50                    min_binding_size: None,
51                },
52                count: None,
53            }],
54        });
55
56        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
57            label: Some("wisp::mask_texture pipeline layout"),
58            bind_group_layouts: &[&bind_group_layout],
59            push_constant_ranges: &[],
60        });
61
62        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
63            label: Some("wisp::mask_texture shader"),
64            source: wgpu::ShaderSource::Wgsl(
65                include_str!("../../shaders/mask_texture.wgsl").into(),
66            ),
67        });
68
69        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
70            label: Some("wisp::mask_texture pipeline"),
71            layout: Some(&pipeline_layout),
72            vertex: wgpu::VertexState {
73                module: &shader,
74                entry_point: Some("main_vs"),
75                buffers: &[],
76                compilation_options: wgpu::PipelineCompilationOptions::default(),
77            },
78            fragment: Some(wgpu::FragmentState {
79                module: &shader,
80                entry_point: Some("main_fs"),
81                targets: &[Some(wgpu::ColorTargetState {
82                    format: output_format,
83                    blend: Some(wgpu::BlendState::REPLACE),
84                    write_mask: wgpu::ColorWrites::ALL,
85                })],
86                compilation_options: wgpu::PipelineCompilationOptions::default(),
87            }),
88            primitive: wgpu::PrimitiveState::default(),
89            depth_stencil: None,
90            multisample: wgpu::MultisampleState::default(),
91            multiview: None,
92            cache: None,
93        });
94
95        Self {
96            pipeline,
97            bind_group_layout,
98        }
99    }
100
101    /// Generate a mask texture for `shape` at `(w, h)` and return it.
102    pub(crate) fn generate(
103        &self,
104        app: &Application,
105        shape: MaskShape,
106        w: u32,
107        h: u32,
108        output_format: wgpu::TextureFormat,
109    ) -> RenderTexture {
110        let rt = RenderTexture::with_format(app, w, h, output_format);
111        self.render_into(app, shape, false, &rt);
112        rt
113    }
114
115    /// Same as [`Self::generate`] but writes into a caller-owned RT
116    /// (used by the cache so it can recycle textures).
117    pub(crate) fn render_into(
118        &self,
119        app: &Application,
120        shape: MaskShape,
121        invert: bool,
122        output: &RenderTexture,
123    ) {
124        let (cx, cy, hx, hy, radius, shape_kind) = match shape {
125            MaskShape::Rect { rect } => {
126                let cx = rect.min.x + rect.size.x * 0.5;
127                let cy = rect.min.y + rect.size.y * 0.5;
128                let hx = (rect.size.x * 0.5).max(0.0);
129                let hy = (rect.size.y * 0.5).max(0.0);
130                (cx, cy, hx, hy, 0.0, 0.0)
131            }
132            MaskShape::RoundedRect { rect, radius } => {
133                let cx = rect.min.x + rect.size.x * 0.5;
134                let cy = rect.min.y + rect.size.y * 0.5;
135                let hx = (rect.size.x * 0.5).max(0.0);
136                let hy = (rect.size.y * 0.5).max(0.0);
137                let r = radius.clamp(0.0, hx.min(hy));
138                (cx, cy, hx, hy, r, 0.0)
139            }
140            MaskShape::Circle { center, radius } => {
141                let r = radius.max(0.0);
142                (center.x, center.y, r, r, r, 0.0)
143            }
144            MaskShape::Ellipse {
145                center,
146                half_extents,
147            } => {
148                let hx = half_extents.x.max(0.0);
149                let hy = half_extents.y.max(0.0);
150                (center.x, center.y, hx, hy, 0.0, 1.0)
151            }
152        };
153
154        let w_f = f32::from(u16::try_from(output.width().min(u32::from(u16::MAX))).unwrap_or(1));
155        let h_f = f32::from(u16::try_from(output.height().min(u32::from(u16::MAX))).unwrap_or(1));
156        let aa = 2.0 / w_f.min(h_f).max(1.0);
157
158        let uniforms = MaskTextureUniforms {
159            center: [cx, cy],
160            half_extents: [hx, hy],
161            radius,
162            aa,
163            invert: if invert { 1.0 } else { 0.0 },
164            shape_kind,
165        };
166        let buffer = app
167            .device()
168            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
169                label: Some("wisp::mask_texture uniforms"),
170                contents: bytemuck::bytes_of(&uniforms),
171                usage: wgpu::BufferUsages::UNIFORM,
172            });
173
174        let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
175            label: Some("wisp::mask_texture bg"),
176            layout: &self.bind_group_layout,
177            entries: &[wgpu::BindGroupEntry {
178                binding: 0,
179                resource: buffer.as_entire_binding(),
180            }],
181        });
182
183        let mut encoder = app
184            .device()
185            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
186                label: Some("wisp::mask_texture encoder"),
187            });
188        {
189            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
190                label: Some("wisp::mask_texture pass"),
191                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
192                    view: output.view(),
193                    resolve_target: None,
194                    ops: wgpu::Operations {
195                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
196                        store: wgpu::StoreOp::Store,
197                    },
198                })],
199                depth_stencil_attachment: None,
200                timestamp_writes: None,
201                occlusion_query_set: None,
202            });
203            pass.set_pipeline(&self.pipeline);
204            pass.set_bind_group(0, &bg, &[]);
205            pass.draw(0..3, 0..1);
206        }
207        app.queue().submit(std::iter::once(encoder.finish()));
208    }
209}