Skip to main content

wisp/render/
path_clip.rs

1//! Path clip pipeline — rasterize a closed-polygon mask for a
2//! `foreground` RT (M-MASK / AUT-35 freehand path mask).
3//!
4//! Uses a uniform-buffered point list (up to 32 vertices for V1) and
5//! a fragment-shader winding-number test (Jordan curve theorem). No
6//! tessellation required, no SDF — just the classic crossings-test
7//! point-in-polygon at every pixel. Works for any simple polygon
8//! (convex or concave); doesn't handle self-intersecting paths
9//! cleanly, but those aren't on the freehand-mask UX path.
10
11use bytemuck::{Pod, Zeroable};
12use wgpu::util::DeviceExt;
13
14use crate::application::Application;
15use crate::texture::render_texture::RenderTexture;
16
17/// Maximum points in a path mask. Mirrors `MAX_POINTS` in
18/// `path_clip.wgsl`.
19pub(crate) const MAX_PATH_POINTS: usize = 32;
20
21#[repr(C)]
22#[derive(Clone, Copy, Pod, Zeroable)]
23struct PathClipUniforms {
24    count: u32,
25    invert: u32,
26    _pad: [u32; 2],
27    /// Each point stored as a `vec4` so the layout matches WGSL's
28    /// `array<vec4<f32>, N>` (stricter alignment than `vec2`). Only
29    /// `.xy` is read by the shader; `.zw` are unused padding.
30    points: [[f32; 4]; MAX_PATH_POINTS],
31}
32
33pub(crate) struct PathClipPipeline {
34    pipeline: wgpu::RenderPipeline,
35    bind_group_layout: wgpu::BindGroupLayout,
36    sampler: wgpu::Sampler,
37}
38
39impl PathClipPipeline {
40    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
41        let device = app.device();
42
43        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
44            label: Some("wisp::path_clip bg layout"),
45            entries: &[
46                wgpu::BindGroupLayoutEntry {
47                    binding: 0,
48                    visibility: wgpu::ShaderStages::FRAGMENT,
49                    ty: wgpu::BindingType::Texture {
50                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
51                        view_dimension: wgpu::TextureViewDimension::D2,
52                        multisampled: false,
53                    },
54                    count: None,
55                },
56                wgpu::BindGroupLayoutEntry {
57                    binding: 1,
58                    visibility: wgpu::ShaderStages::FRAGMENT,
59                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
60                    count: None,
61                },
62                wgpu::BindGroupLayoutEntry {
63                    binding: 2,
64                    visibility: wgpu::ShaderStages::FRAGMENT,
65                    ty: wgpu::BindingType::Buffer {
66                        ty: wgpu::BufferBindingType::Uniform,
67                        has_dynamic_offset: false,
68                        min_binding_size: None,
69                    },
70                    count: None,
71                },
72            ],
73        });
74
75        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
76            label: Some("wisp::path_clip pipeline layout"),
77            bind_group_layouts: &[&bind_group_layout],
78            push_constant_ranges: &[],
79        });
80
81        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
82            label: Some("wisp::path_clip shader"),
83            source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/path_clip.wgsl").into()),
84        });
85
86        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
87            label: Some("wisp::path_clip pipeline"),
88            layout: Some(&pipeline_layout),
89            vertex: wgpu::VertexState {
90                module: &shader,
91                entry_point: Some("main_vs"),
92                buffers: &[],
93                compilation_options: wgpu::PipelineCompilationOptions::default(),
94            },
95            fragment: Some(wgpu::FragmentState {
96                module: &shader,
97                entry_point: Some("main_fs"),
98                targets: &[Some(wgpu::ColorTargetState {
99                    format: output_format,
100                    blend: Some(wgpu::BlendState::REPLACE),
101                    write_mask: wgpu::ColorWrites::ALL,
102                })],
103                compilation_options: wgpu::PipelineCompilationOptions::default(),
104            }),
105            primitive: wgpu::PrimitiveState::default(),
106            depth_stencil: None,
107            multisample: wgpu::MultisampleState::default(),
108            multiview: None,
109            cache: None,
110        });
111
112        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
113            label: Some("wisp::path_clip sampler"),
114            address_mode_u: wgpu::AddressMode::ClampToEdge,
115            address_mode_v: wgpu::AddressMode::ClampToEdge,
116            address_mode_w: wgpu::AddressMode::ClampToEdge,
117            mag_filter: wgpu::FilterMode::Linear,
118            min_filter: wgpu::FilterMode::Linear,
119            mipmap_filter: wgpu::FilterMode::Nearest,
120            ..Default::default()
121        });
122
123        Self {
124            pipeline,
125            bind_group_layout,
126            sampler,
127        }
128    }
129
130    /// Sample `foreground` and write `foreground × point_in_polygon`
131    /// into `output`. `points` is the closed polygon in NDC; up to
132    /// `MAX_PATH_POINTS` entries are honored, the rest are ignored.
133    pub(crate) fn apply(
134        &self,
135        app: &Application,
136        points: &[glam::Vec2],
137        invert: bool,
138        foreground: &RenderTexture,
139        output: &RenderTexture,
140    ) {
141        let mut padded = [[0.0_f32; 4]; MAX_PATH_POINTS];
142        let count = points.len().min(MAX_PATH_POINTS);
143        for (slot, p) in padded.iter_mut().zip(points.iter()).take(count) {
144            *slot = [p.x, p.y, 0.0, 0.0];
145        }
146        let count_u32 = u32::try_from(count).unwrap_or(0);
147
148        let uniforms = PathClipUniforms {
149            count: count_u32,
150            invert: u32::from(invert),
151            _pad: [0, 0],
152            points: padded,
153        };
154        let buffer = app
155            .device()
156            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
157                label: Some("wisp::path_clip uniforms"),
158                contents: bytemuck::bytes_of(&uniforms),
159                usage: wgpu::BufferUsages::UNIFORM,
160            });
161
162        let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
163            label: Some("wisp::path_clip bg"),
164            layout: &self.bind_group_layout,
165            entries: &[
166                wgpu::BindGroupEntry {
167                    binding: 0,
168                    resource: wgpu::BindingResource::TextureView(foreground.view()),
169                },
170                wgpu::BindGroupEntry {
171                    binding: 1,
172                    resource: wgpu::BindingResource::Sampler(&self.sampler),
173                },
174                wgpu::BindGroupEntry {
175                    binding: 2,
176                    resource: buffer.as_entire_binding(),
177                },
178            ],
179        });
180
181        let mut encoder = app
182            .device()
183            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
184                label: Some("wisp::path_clip encoder"),
185            });
186        {
187            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
188                label: Some("wisp::path_clip pass"),
189                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
190                    view: output.view(),
191                    resolve_target: None,
192                    ops: wgpu::Operations {
193                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
194                        store: wgpu::StoreOp::Store,
195                    },
196                })],
197                depth_stencil_attachment: None,
198                timestamp_writes: None,
199                occlusion_query_set: None,
200            });
201            pass.set_pipeline(&self.pipeline);
202            pass.set_bind_group(0, &bg, &[]);
203            pass.draw(0..3, 0..1);
204        }
205        app.queue().submit(std::iter::once(encoder.finish()));
206    }
207}