feather_ui/render/
shape.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use super::compositor;
5use crate::color::sRGB;
6use crate::component::shape::ShapeKind;
7use crate::graphics::{self, Vec2f, Vec4f};
8use crate::render::atlas::{self, Atlas};
9use crate::render::compositor::CompositorView;
10use crate::{Canonicalize, PxDim, PxPoint, SourceID, shaders};
11use core::f32;
12use guillotiere::euclid::Size2D;
13use num_traits::Zero;
14use std::collections::HashMap;
15use std::num::NonZero;
16use std::sync::Arc;
17use wgpu::BindGroupLayout;
18
19pub struct Instance<const KIND: u8> {
20    pub padding: crate::PxPerimeter,
21    pub border: f32,
22    pub blur: f32,
23    pub fill: sRGB,
24    pub outline: sRGB,
25    pub corners: [f32; 4],
26    pub id: Arc<SourceID>,
27}
28
29impl<const KIND: u8> super::Renderable for Instance<KIND> {
30    fn render(
31        &self,
32        area: crate::PxRect,
33        driver: &crate::graphics::Driver,
34        compositor: &mut CompositorView<'_>,
35    ) -> Result<(), crate::Error> {
36        let dim = area.dim() - self.padding.bottomright() - self.padding.topleft();
37
38        if dim.width <= 0.0 || dim.height <= 0.0 {
39            return Ok(());
40        }
41
42        // If a rect has no corners and no outline, it's just a flat color box and we
43        // can draw it directly in the compositor
44        if self.corners.iter().all(|x| x.is_zero()) && self.border.is_zero() {
45            compositor.append_data(
46                area.topleft().add_size(&self.padding.topleft()),
47                dim,
48                [0.0, 0.0].into(),
49                [0.0, 0.0].into(),
50                self.fill.as_32bit().rgba,
51                0.0,
52                u8::MAX,
53                false,
54            );
55
56            return Ok(());
57        }
58
59        let perimeter = [
60            dim.height - self.corners[0] - self.corners[3],
61            dim.width - self.corners[0] - self.corners[1],
62            dim.height - self.corners[1] - self.corners[2],
63            dim.width - self.corners[2] - self.corners[3],
64        ];
65
66        // RoundRects have a specific optimization, but only if no edge length is less
67        // than 2 pixels
68        if KIND == ShapeKind::RoundRect as u8 && perimeter.iter().all(|x| *x >= 2.0) {
69            // If the border is larger than the corner itself, pretend the size of that
70            // corner is the border.
71            let mut corners = self.corners.map(|x| x.max(self.border));
72            let mut intcorners = corners.map(|x| x.ceil() as i32);
73
74            let intsides = [
75                intcorners[0].max(intcorners[3]),
76                intcorners[0].max(intcorners[1]),
77                intcorners[1].max(intcorners[2]),
78                intcorners[2].max(intcorners[3]),
79            ];
80
81            // Here we generate a rounded block equal to exactly left side + 3 pixels +
82            // right side
83            let inner = atlas::Size::new(
84                intsides[0] + intsides[2] + 3, // left + right
85                intsides[1] + intsides[3] + 3, // top + bottom
86            );
87
88            // We reserve an additional 2 pixel border around each side of our rect for
89            // sampling purposes. It must be 2 pixels because we have to inflate
90            // the rect by 1 pixel for fractional draws already, which means we
91            // need an additional transparent pixel of buffer to cover all possible sampling
92            // scenarios.
93            let (region_uv, region_index) = driver
94                .with_pipeline::<Shape<KIND>, Result<(atlas::PxBox, u8), crate::Error>>(
95                    |pipeline| {
96                        pipeline.reserve(
97                            driver,
98                            self.id.clone(),
99                            inner + atlas::Size::new(4, 4),
100                            Data {
101                                pos: [2.0; 2].into(),
102                                dim: inner.to_f32().to_array().into(),
103                                border: self.border,
104                                blur: self.blur,
105                                // We use corners raised to the nearest pixel so we can cut out the
106                                // corners neatly
107                                corners: intcorners.map(|x| x as f32).into(),
108                                fill: self.fill.as_32bit().rgba,
109                                outline: self.outline.as_32bit().rgba,
110                            },
111                            true,
112                        )
113                    },
114                )?;
115
116            // The only reason this works is because we set the uvdim here to 0 on the axis
117            // that is being extended, which ensures no interpolation of the UV
118            // coordinate happens along that axis
119
120            // We add data here starting from the topleft corner and going clockwise around
121            // the rect: 1 6 2
122            // 5 9 7
123            // 4 8 3
124
125            // Pretend all corners are 1 pixel larger (this works because our buffer is 3
126            // pixels)
127            corners = corners.map(|x| x + 1.0);
128            intcorners = intcorners.map(|x| x + 1);
129
130            let topleft = area.topleft().add_size(&self.padding.topleft()).to_vector();
131            // Add 2 to account for the 2 pixel transparent border
132            let uvpos = region_uv.min.add_size(&Size2D::splat(2));
133            let mut gen_corner = |pos: PxPoint, corner: f32, u: i32, v: i32| {
134                let intdim = PxDim::splat(corner.ceil());
135                compositor.append_data(
136                    pos + topleft,
137                    PxDim::splat(corner),
138                    uvpos
139                        .add_size(&Size2D::new(u, v))
140                        .to_f32()
141                        .to_array()
142                        .into(),
143                    intdim.to_array().into(),
144                    0xFFFFFFFF,
145                    0.0,
146                    region_index,
147                    true,
148                );
149            };
150
151            // This is nontrivial, because this must be assembled in raw mode, which means
152            // we must do the directional inflation here ourselves. This amounts
153            // to changing every 0 into a -1, but *not* changing the non-zero positions and
154            // instead adding 1 to the dimensions, which on corners means adding
155            // yet another +1 to the corner size.
156            gen_corner(PxPoint::new(-1.0, -1.0), corners[0] + 1.0, -1, -1);
157            gen_corner(
158                PxPoint::new(dim.width - corners[1], -1.0),
159                corners[1] + 1.0,
160                inner.width - intcorners[1],
161                -1,
162            );
163            gen_corner(
164                PxPoint::new(dim.width - corners[2], dim.height - corners[2]),
165                corners[2] + 1.0,
166                inner.width - intcorners[2],
167                inner.height - intcorners[2],
168            );
169            gen_corner(
170                PxPoint::new(-1.0, dim.height - corners[3]),
171                corners[3] + 1.0,
172                -1,
173                inner.height - intcorners[3],
174            );
175
176            let sides = crate::PxRect::new(
177                corners[0].max(corners[3]),
178                corners[0].max(corners[1]),
179                corners[1].max(corners[2]),
180                corners[2].max(corners[3]),
181            );
182
183            // We can't just do sides.ceil() because the result is not the same as ceiling
184            // both corners and adding them.
185            let intsides = [
186                intcorners[0].max(intcorners[3]),
187                intcorners[0].max(intcorners[1]),
188                intcorners[1].max(intcorners[2]),
189                intcorners[2].max(intcorners[3]),
190            ];
191
192            let mut gen_side = |dim: PxDim, pos: PxPoint, u: i32, v: i32, w: i32, h: i32| {
193                compositor.append_data(
194                    pos + topleft,
195                    dim,
196                    uvpos
197                        .add_size(&Size2D::new(u, v))
198                        .to_f32()
199                        .to_array()
200                        .into(),
201                    [w as f32, h as f32].into(),
202                    0xFFFFFFFF,
203                    0.0,
204                    region_index,
205                    true,
206                );
207            };
208
209            // Left Top Right Bottom side order
210            // Once again, we must manually inflate the sides here, but these are more
211            // tricky. To make it a bit easier, we only inflate exactly the one
212            // pixel of the side that actually matters.
213            gen_side(
214                PxDim::new(sides.left() + 1.0, dim.height - corners[0] - corners[3]),
215                PxPoint::new(-1.0, corners[0]),
216                -1,
217                intsides[1],
218                intsides[0] + 1,
219                0,
220            );
221            gen_side(
222                PxDim::new(dim.width - corners[0] - corners[1], sides.top() + 1.0),
223                PxPoint::new(corners[0], -1.0),
224                intsides[0],
225                -1,
226                0,
227                intsides[1] + 1,
228            );
229            gen_side(
230                PxDim::new(sides.right() + 1.0, dim.height - corners[1] - corners[2]),
231                PxPoint::new(dim.width - sides.right(), corners[1]),
232                inner.width - intsides[2],
233                intsides[1],
234                intsides[2] + 1,
235                0,
236            );
237            gen_side(
238                PxDim::new(dim.width - corners[3] - corners[2], sides.bottom() + 1.0),
239                PxPoint::new(corners[3], dim.height - sides.bottom()),
240                intsides[0],
241                inner.height - intsides[3],
242                0,
243                intsides[3] + 1,
244            );
245
246            // Inner area is just a flat color
247            compositor.append_data(
248                PxPoint::splat(corners[0]) + topleft,
249                dim - PxDim::splat(corners[0] + corners[2]),
250                [0.0, 0.0].into(),
251                [0.0, 0.0].into(),
252                self.fill.as_32bit().rgba,
253                0.0,
254                u8::MAX,
255                true,
256            );
257
258            return Ok(());
259        }
260
261        // The region dimensions here can be wrong, because the region is rounded up to
262        // the nearest pixel. However, properly fixing this requires changing
263        // how the SDF shader works so it can properly emulate conservative
264        // rasterization. For now, we keep our original behavior of rounding up and
265        // then letting the compositor squish the result slightly, which is actually
266        // pretty accurate. TODO: Change this to be pixel-perfect by outputting
267        // the exact dimensions instead of rounded ones.
268
269        let (region_uv, region_index) = driver
270            .with_pipeline::<Shape<KIND>, Result<(atlas::PxBox, u8), crate::Error>>(|pipeline| {
271                pipeline.reserve(
272                    driver,
273                    self.id.clone(),
274                    dim.ceil().cast(),
275                    Data {
276                        pos: [0.0; 2].into(),
277                        dim: dim.to_array().into(),
278                        border: self.border,
279                        blur: self.blur,
280                        corners: self.corners.into(),
281                        fill: self.fill.as_32bit().rgba,
282                        outline: self.outline.as_32bit().rgba,
283                    },
284                    false,
285                )
286            })?;
287
288        compositor.append_data(
289            area.topleft().add_size(&self.padding.topleft()),
290            dim,
291            region_uv.min.to_f32().to_array().into(),
292            region_uv.size().to_f32().to_array().into(),
293            0xFFFFFFFF,
294            0.0,
295            region_index,
296            false,
297        );
298
299        Ok(())
300    }
301}
302
303// Renderdoc Format:
304// struct Data {
305// 	float corners[4];
306// 	float pos[2];
307// 	float dim[2];
308// 	float border;
309// 	float blur;
310// 	uint32_t fill;
311// 	uint32_t outline;
312// };
313// Data d[];
314
315// TODO: Maybe use NotNaN from ordered_float if this doesn't mess up alignment?
316#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::NoUninit)]
317#[repr(C)]
318pub struct Data {
319    pub corners: Vec4f,
320    pub pos: Vec2f,
321    pub dim: Vec2f,
322    pub border: f32,
323    pub blur: f32,
324    pub fill: u32,
325    pub outline: u32,
326}
327
328// We manually implement Eq because no NaNs should be in Data
329impl Eq for Data {}
330
331impl std::hash::Hash for Data {
332    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
333        self.corners.hash(state);
334        self.pos.hash(state);
335        self.dim.hash(state);
336        self.border.canonical_bits().hash(state);
337        self.blur.canonical_bits().hash(state);
338        self.fill.hash(state);
339        self.outline.hash(state);
340    }
341}
342
343#[derive(Debug)]
344pub struct Shape<const KIND: u8> {
345    data: HashMap<u8, Vec<Data>>,
346    buffer: wgpu::Buffer,
347    pipeline: wgpu::RenderPipeline,
348    group: wgpu::BindGroup,
349    cache: HashMap<Arc<SourceID>, (Data, atlas::PxBox, u8)>,
350    refcount: HashMap<(Data, atlas::Size), (atlas::Region, usize)>,
351}
352
353impl<const KIND: u8> Shape<KIND> {
354    fn reserve(
355        &mut self,
356        driver: &graphics::Driver,
357        id: Arc<SourceID>,
358        uvdim: atlas::Size,
359        mut data: Data,
360        clear: bool,
361    ) -> Result<(atlas::PxBox, u8), crate::Error> {
362        // First we check our ID cache to see if there's an existing entry
363        if let Some((cache, uv, layer)) = self.cache.get(&id) {
364            // If we already have data cached, see if it changed. If it didn't, just append
365            // the cached data.
366            if data == *cache && uvdim == uv.size() {
367                // To make the cache possible, the data only contains the offset, so we add the
368                // region position here.
369                data.pos += uv.min.to_f32().to_array();
370                self.data.entry(*layer).or_default().push(data);
371                return Ok((*uv, *layer));
372            } else if let Some((old, uv, _)) = self.cache.remove(&id) {
373                // Otherwise, we have to delete the cache and decrement the refcount. If the
374                // refcount reaches 0, we delete the region entirely.
375                if let std::collections::hash_map::Entry::Occupied(mut v) =
376                    self.refcount.entry((old, uv.size()))
377                {
378                    if v.get().1 <= 1 {
379                        driver.atlas.write().destroy(&mut v.get_mut().0);
380                        v.remove();
381                    } else {
382                        v.get_mut().1 -= 1;
383                    }
384                }
385            }
386        }
387
388        // If we get this far, either we didn't have something cached, or it had to be
389        // replaced. We check to see if the data key we have is already being
390        // used for something else, and increment the refcount if so. Otherwise, we
391        // allocate a new region.
392        let (region, _) = match self.refcount.entry((data, uvdim)) {
393            std::collections::hash_map::Entry::Occupied(mut occupied_entry) => {
394                occupied_entry.get_mut().1 += 1;
395                occupied_entry.into_mut()
396            }
397            std::collections::hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert((
398                driver.atlas.write().reserve(
399                    &driver.device,
400                    uvdim,
401                    None,
402                    if clear { Some(&driver.queue) } else { None },
403                )?,
404                1,
405            )),
406        };
407
408        debug_assert_eq!(uvdim, region.uv.size());
409
410        self.cache
411            .entry(id)
412            .and_modify(|v| *v = (data, region.uv, region.index))
413            .or_insert((data, region.uv, region.index));
414
415        data.pos += region.uv.min.to_f32().to_array();
416        self.data.entry(region.index).or_default().push(data);
417        Ok((region.uv, region.index))
418    }
419}
420
421impl<const KIND: u8> super::Pipeline for Shape<KIND> {
422    fn draw(&mut self, driver: &graphics::Driver, pass: &mut wgpu::RenderPass<'_>, layer: u8) {
423        if let Some(data) = self.data.get_mut(&layer) {
424            let size = data.len() * size_of::<Data>();
425            if (self.buffer.size() as usize) < size {
426                self.buffer.destroy();
427                self.buffer = driver.device.create_buffer(&wgpu::BufferDescriptor {
428                    label: Some("Shape Data"),
429                    size: size.next_power_of_two() as u64,
430                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
431                    mapped_at_creation: false,
432                });
433                self.group = Self::rebind(
434                    &self.buffer,
435                    &self.pipeline.get_bind_group_layout(0),
436                    &driver.device,
437                    &driver.atlas.read(),
438                );
439            }
440
441            driver
442                .queue
443                .write_buffer(&self.buffer, 0, bytemuck::cast_slice(data.as_slice()));
444
445            pass.set_pipeline(&self.pipeline);
446            pass.set_bind_group(0, &self.group, &[0]);
447            pass.draw(0..(data.len() as u32 * 6), 0..1);
448            data.clear();
449        }
450    }
451
452    fn destroy(&mut self, driver: &graphics::Driver) {
453        for (_, (mut region, _)) in self.refcount.drain() {
454            driver.atlas.write().destroy(&mut region);
455        }
456    }
457}
458
459impl<const KIND: u8> Shape<KIND> {
460    pub fn layout(device: &wgpu::Device) -> wgpu::PipelineLayout {
461        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
462            label: Some("Shape Bind Group"),
463            entries: &[
464                wgpu::BindGroupLayoutEntry {
465                    binding: 0,
466                    visibility: wgpu::ShaderStages::VERTEX,
467                    ty: wgpu::BindingType::Buffer {
468                        ty: wgpu::BufferBindingType::Uniform,
469                        has_dynamic_offset: false,
470                        min_binding_size: NonZero::new(size_of::<crate::Mat4x4>() as u64),
471                    },
472                    count: None,
473                },
474                wgpu::BindGroupLayoutEntry {
475                    binding: 1,
476                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
477                    ty: wgpu::BindingType::Buffer {
478                        ty: wgpu::BufferBindingType::Storage { read_only: true },
479                        has_dynamic_offset: true,
480                        min_binding_size: None,
481                    },
482                    count: None,
483                },
484                wgpu::BindGroupLayoutEntry {
485                    binding: 2,
486                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
487                    ty: wgpu::BindingType::Buffer {
488                        ty: wgpu::BufferBindingType::Uniform,
489                        has_dynamic_offset: false,
490                        min_binding_size: NonZero::new(size_of::<u32>() as u64),
491                    },
492                    count: None,
493                },
494            ],
495        });
496
497        device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
498            label: Some("Shape Pipeline"),
499            bind_group_layouts: &[&bind_group_layout],
500            push_constant_ranges: &[],
501        })
502    }
503
504    pub fn shader(device: &wgpu::Device) -> wgpu::ShaderModule {
505        shaders::load_wgsl(device, "Shape", shaders::get("shape.wgsl").unwrap())
506    }
507
508    fn pipeline(
509        layout: &wgpu::PipelineLayout,
510        shader: &wgpu::ShaderModule,
511        device: &wgpu::Device,
512        entry_point: &str,
513    ) -> wgpu::RenderPipeline {
514        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
515            label: None,
516            layout: Some(layout),
517            vertex: wgpu::VertexState {
518                module: shader,
519                entry_point: Some("vs_main"),
520                buffers: &[],
521                compilation_options: Default::default(),
522            },
523            fragment: Some(wgpu::FragmentState {
524                module: shader,
525                entry_point: Some(entry_point),
526                compilation_options: Default::default(),
527                targets: &[Some(compositor::TARGET_BLEND)],
528            }),
529            primitive: wgpu::PrimitiveState {
530                front_face: wgpu::FrontFace::Cw,
531                topology: wgpu::PrimitiveTopology::TriangleList,
532                ..Default::default()
533            },
534            depth_stencil: None,
535            multisample: wgpu::MultisampleState::default(),
536            multiview: None,
537            cache: None,
538        })
539    }
540
541    fn rebind(
542        buffer: &wgpu::Buffer,
543        layout: &BindGroupLayout,
544        device: &wgpu::Device,
545        atlas: &Atlas,
546    ) -> wgpu::BindGroup {
547        let bindings = [
548            wgpu::BindGroupEntry {
549                binding: 0,
550                resource: atlas.mvp.as_entire_binding(),
551            },
552            wgpu::BindGroupEntry {
553                binding: 1,
554                resource: buffer.as_entire_binding(),
555            },
556            wgpu::BindGroupEntry {
557                binding: 2,
558                resource: atlas.extent_buf.as_entire_binding(),
559            },
560        ];
561
562        device.create_bind_group(&wgpu::BindGroupDescriptor {
563            layout,
564            entries: &bindings,
565            label: None,
566        })
567    }
568
569    fn new(
570        layout: &wgpu::PipelineLayout,
571        shader: &wgpu::ShaderModule,
572        driver: &graphics::Driver,
573        entry_point: &str,
574    ) -> Self {
575        let buffer = driver.device.create_buffer(&wgpu::BufferDescriptor {
576            label: Some("Shape Data"),
577            size: 32 * size_of::<Data>() as u64,
578            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
579            mapped_at_creation: false,
580        });
581        let pipeline = Self::pipeline(layout, shader, &driver.device, entry_point);
582
583        let group = Self::rebind(
584            &buffer,
585            &pipeline.get_bind_group_layout(0),
586            &driver.device,
587            &driver.atlas.read(),
588        );
589
590        Self {
591            data: HashMap::new(),
592            buffer,
593            pipeline,
594            group,
595            cache: HashMap::new(),
596            refcount: HashMap::new(),
597        }
598    }
599}
600
601impl Shape<0> {
602    pub fn create(
603        layout: &wgpu::PipelineLayout,
604        shader: &wgpu::ShaderModule,
605        driver: &graphics::Driver,
606    ) -> Box<dyn super::Pipeline> {
607        Box::new(Self::new(layout, shader, driver, "rectangle"))
608    }
609}
610
611impl Shape<1> {
612    pub fn create(
613        layout: &wgpu::PipelineLayout,
614        shader: &wgpu::ShaderModule,
615        driver: &graphics::Driver,
616    ) -> Box<dyn super::Pipeline> {
617        Box::new(Self::new(layout, shader, driver, "triangle"))
618    }
619}
620
621impl Shape<2> {
622    pub fn create(
623        layout: &wgpu::PipelineLayout,
624        shader: &wgpu::ShaderModule,
625        driver: &graphics::Driver,
626    ) -> Box<dyn super::Pipeline> {
627        Box::new(Self::new(layout, shader, driver, "circle"))
628    }
629}
630
631impl Shape<3> {
632    pub fn create(
633        layout: &wgpu::PipelineLayout,
634        shader: &wgpu::ShaderModule,
635        driver: &graphics::Driver,
636    ) -> Box<dyn super::Pipeline> {
637        Box::new(Self::new(layout, shader, driver, "arcs"))
638    }
639}